package org.zstack.core.workflow;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.zstack.core.Platform;
import org.zstack.core.db.DatabaseFacade;
import org.zstack.core.db.SimpleQuery;
import org.zstack.core.db.SimpleQuery.Od;
import org.zstack.core.db.SimpleQuery.Op;
import org.zstack.core.errorcode.ErrorFacade;
import org.zstack.core.statemachine.StateMachine;
import org.zstack.header.errorcode.ErrorCode;
import org.zstack.header.exception.CloudRuntimeException;
import org.zstack.utils.Utils;
import org.zstack.utils.logging.CLogger;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Configurable(preConstruction = true, autowire = Autowire.BY_TYPE)
public class WorkFlowChain {
protected static final CLogger logger = Utils.getLogger(WorkFlowChain.class);
protected static final StateMachine<WorkFlowChainState, WorkFlowChainStateEvent> chainStates;
protected static final StateMachine<WorkFlowState, WorkFlowStateEvent> flowState;
static {
chainStates = Platform.<WorkFlowChainState, WorkFlowChainStateEvent> createStateMachine();
chainStates.addTranscation(WorkFlowChainState.Processing, WorkFlowChainStateEvent.done, WorkFlowChainState.ProcessDone);
chainStates.addTranscation(WorkFlowChainState.Processing, WorkFlowChainStateEvent.failed, WorkFlowChainState.ProcessFailed);
chainStates.addTranscation(WorkFlowChainState.ProcessFailed, WorkFlowChainStateEvent.rollbackDone, WorkFlowChainState.RollbackDone);
chainStates.addTranscation(WorkFlowChainState.ProcessDone, WorkFlowChainStateEvent.rollbackDone, WorkFlowChainState.RollbackDone);
flowState = Platform.<WorkFlowState, WorkFlowStateEvent> createStateMachine();
flowState.addTranscation(WorkFlowState.Processing, WorkFlowStateEvent.done, WorkFlowState.Done);
flowState.addTranscation(WorkFlowState.Processing, WorkFlowStateEvent.failed, WorkFlowState.Failed);
flowState.addTranscation(WorkFlowState.Failed, WorkFlowStateEvent.rollbackDone, WorkFlowState.RollbackDone);
flowState.addTranscation(WorkFlowState.Done, WorkFlowStateEvent.rollbackDone, WorkFlowState.RollbackDone);
}
protected enum ContinueStrategy {
Restart, Nothing, Rollback,
}
@Autowired
protected DatabaseFacade dbf;
@Autowired
protected ErrorFacade errf;
protected String name;
protected String owner;
protected WorkFlowContext context;
protected List<WorkFlow> flows = new ArrayList<WorkFlow>();
protected String uuid;
protected WorkFlowChainVO chainvo;
public WorkFlowChain(String name) {
this.name = name;
}
public WorkFlowChain() {
this.name = "zstack";
}
public WorkFlowChain add(WorkFlow flow) {
flows.add(flow);
return this;
}
public WorkFlowChain build() {
if (this.owner == null) {
this.owner = "zstack";
}
if (flows.isEmpty()) {
throw new IllegalArgumentException("WorkFlowChain cannot be built without adding any WorkFlow in it");
}
StringBuilder sb = new StringBuilder(getName());
sb.append(getOwner());
for (WorkFlow f : flows) {
String name = f.getName();
name = name == null ? f.getClass().getCanonicalName() : name;
sb.append(name);
}
uuid = UUID.nameUUIDFromBytes(sb.toString().getBytes()).toString().replace("-", "");
return this;
}
public String getUuid() {
return uuid;
}
public String getName() {
return name;
}
public String getOwner() {
return owner;
}
public WorkFlowChain setOwner(String owner) {
this.owner = owner;
return this;
}
public WorkFlowChain setName(String name) {
this.name = name;
return this;
}
protected void initialize() {
if (getUuid() == null) {
throw new IllegalArgumentException("WorkFlowChain cannot run before WorkFlowChain.build() is called");
}
WorkFlowChainVO cvo = new WorkFlowChainVO();
cvo.setName(getName());
cvo.setOwner(owner);
cvo.setUuid(getUuid());
cvo.setState(WorkFlowChainState.Processing);
cvo.setCurrentPosition(0);
cvo.setTotalWorkFlows(flows.size());
chainvo = dbf.persistAndRefresh(cvo);
}
protected ErrorCode processFlow(WorkFlow flow, WorkFlowVO vo, int position) {
if (vo == null) {
vo = new WorkFlowVO();
}
vo.setChainUuid(chainvo.getUuid());
vo.setName(flow.getName());
vo.setState(WorkFlowState.Processing);
vo.setContext(context.toBytes());
vo.setPosition(position);
vo = dbf.updateAndRefresh(vo);
try {
flow.process(context);
vo.setState(flowState.getNextState(vo.getState(), WorkFlowStateEvent.done));
vo.setContext(context.toBytes());
dbf.update(vo);
logger.debug(String.format("Successfully processed workflow[%s] in chain[%s]", flow.getName(), getName()));
return null;
} catch (WorkFlowException e) {
vo.setReason(e.getErrorCode().toString());
vo.setState(flowState.getNextState(vo.getState(), WorkFlowStateEvent.failed));
logger.debug(String.format("workflow[%s] in chain[%s] failed because %s", flow.getName(), getName(), e.getErrorCode()));
dbf.update(vo);
return e.getErrorCode();
} catch (Throwable t) {
ErrorCode err = errf.throwableToInternalError(t);
vo.setReason(err.toString());
vo.setState(flowState.getNextState(vo.getState(), WorkFlowStateEvent.failed));
logger.debug(String.format("workflow[%s] in chain[%s] failed because of an unhandle exception", flow.getName(), getName()), t);
dbf.update(vo);
return err;
}
}
protected void rollbackFlow(WorkFlowVO vo) {
WorkFlow flow = flows.get(vo.getPosition());
WorkFlowContext ctx = WorkFlowContext.fromBytes(vo.getContext());
try {
flow.rollback(ctx);
logger.debug(String.format("Successfully rolled back workflow[%s] in chain[%s]", flow.getName(), getName()));
} catch (Throwable t) {
logger.warn(String.format("Unhandled exception happend while rolling back workflow[%s] in chain[%s]", flow.getName(), getName()), t);
}
vo.setState(flowState.getNextState(vo.getState(), WorkFlowStateEvent.rollbackDone));
dbf.update(vo);
}
protected void rollback() {
SimpleQuery<WorkFlowVO> query = dbf.createQuery(WorkFlowVO.class);
query.add(WorkFlowVO_.chainUuid, Op.EQ, chainvo.getUuid());
query.orderBy(WorkFlowVO_.position, Od.DESC);
List<WorkFlowVO> vos = query.list();
for (WorkFlowVO vo : vos) {
if (vo.getState() == WorkFlowState.RollbackDone) {
/* when this is called from carryOn(), some flows may have been rolled back, skip them */
continue;
}
rollbackFlow(vo);
}
chainvo.setState(chainStates.getNextState(chainvo.getState(), WorkFlowChainStateEvent.rollbackDone));
chainvo = dbf.updateAndRefresh(chainvo);
logger.debug(String.format("Rolled back all flows in workflow chain[%s]", getName()));
}
public WorkFlowContext run() throws WorkFlowException {
return run(null);
}
public WorkFlowContext run(WorkFlowContext ctx) throws WorkFlowException {
if (ctx == null) {
ctx = new WorkFlowContext();
}
initialize();
this.context = ctx;
ErrorCode err = null;
for (int i = 0; i < flows.size(); i++) {
WorkFlow flow = flows.get(i);
err = processFlow(flow, null, i);
if (err != null) {
chainvo.setReason(err.toString());
chainvo.setState(chainStates.getNextState(chainvo.getState(), WorkFlowChainStateEvent.failed));
chainvo.setCurrentPosition(i);
chainvo = dbf.updateAndRefresh(chainvo);
break;
} else {
chainvo.setCurrentPosition(i);
chainvo = dbf.updateAndRefresh(chainvo);
}
}
if (err != null) {
logger.debug(String.format("Starting to roll back all flows in chain[%s]", getName()));
rollback();
throw new WorkFlowException(err);
} else {
chainvo.setState(WorkFlowChainState.ProcessDone);
chainvo = dbf.updateAndRefresh(chainvo);
}
return this.context;
}
protected WorkFlowContext run(WorkFlowContext ctx, WorkFlowVO vo) throws WorkFlowException {
this.context = ctx;
ErrorCode err = null;
int startPosition = vo.getState() == WorkFlowState.Done ? vo.getPosition() + 1 : vo.getPosition();
logger.debug(String.format("Restart flows in work flow chain[uuid:%s], start position is %s", chainvo.getUuid(), startPosition));
for (int i = startPosition; i < flows.size(); i++) {
WorkFlow flow = flows.get(i);
if (vo.getPosition() == i) {
err = processFlow(flow, vo, i);
} else {
err = processFlow(flow, null, i);
}
if (err != null) {
chainvo.setReason(err.toString());
chainvo.setState(chainStates.getNextState(chainvo.getState(), WorkFlowChainStateEvent.failed));
chainvo.setCurrentPosition(i);
chainvo = dbf.updateAndRefresh(chainvo);
break;
} else {
chainvo.setCurrentPosition(i);
chainvo = dbf.updateAndRefresh(chainvo);
}
}
if (err != null) {
logger.debug(String.format("Starting to roll back all flows in chain[%s]", getName()));
rollback();
throw new WorkFlowException(err);
} else {
chainvo.setState(WorkFlowChainState.ProcessDone);
chainvo = dbf.updateAndRefresh(chainvo);
}
return this.context;
}
protected ContinueStrategy getContinueStrategy() {
if (chainvo.getState() == WorkFlowChainState.ProcessDone || chainvo.getState() == WorkFlowChainState.RollbackDone) {
return ContinueStrategy.Nothing;
} else if (chainvo.getState() == WorkFlowChainState.ProcessFailed) {
return ContinueStrategy.Rollback;
} else if (chainvo.getState() == WorkFlowChainState.Processing) {
SimpleQuery<WorkFlowVO> query = dbf.createQuery(WorkFlowVO.class);
query.add(WorkFlowVO_.chainUuid, Op.EQ, chainvo.getUuid());
query.orderBy(WorkFlowVO_.position, Od.DESC);
List<WorkFlowVO> vos = query.list();
if (vos.isEmpty()) {
return ContinueStrategy.Restart;
}
WorkFlowVO last = vos.get(0);
if (last.getState() == WorkFlowState.Processing) {
return ContinueStrategy.Restart;
} else if (last.getState() == WorkFlowState.Done) {
return last.getPosition() == chainvo.getTotalWorkFlows() - 1 ? ContinueStrategy.Nothing : ContinueStrategy.Restart;
} else if (last.getState() == WorkFlowState.RollbackDone) {
WorkFlowVO first = vos.get(vos.size() - 1);
return first.getState() == WorkFlowState.RollbackDone ? ContinueStrategy.Nothing : ContinueStrategy.Rollback;
} else if (last.getState() == WorkFlowState.Failed) {
return ContinueStrategy.Rollback;
}
}
throw new CloudRuntimeException(String.format("Program error: cannot find ContinueStrategy for work flow chain[uuid:%s]", chainvo.getUuid()));
}
public WorkFlowContext carryOn(String chainUuid) throws WorkFlowException {
SimpleQuery<WorkFlowChainVO> query = dbf.createQuery(WorkFlowChainVO.class);
query.add(WorkFlowChainVO_.uuid, Op.EQ, chainUuid);
chainvo = query.find();
if (chainvo == null) {
throw new IllegalArgumentException(String.format("Cannot find workflow chain[uuid:%s]", chainUuid));
}
this.name = chainvo.getName();
this.owner = chainvo.getOwner();
this.uuid = chainvo.getUuid();
ContinueStrategy nextStep = getContinueStrategy();
if (nextStep == ContinueStrategy.Nothing) {
return carryOnNothing();
} else if (nextStep == ContinueStrategy.Restart) {
return carryOnRestart();
} else if (nextStep == ContinueStrategy.Rollback) {
return carryOnRollback();
}
return null;
}
protected WorkFlowContext carryOnRollback() {
logger.debug(String.format("Restart to roll back flows in work flow chain[uuid:%s]", chainvo.getUuid()));
rollback();
return null;
}
protected WorkFlowContext carryOnRestart() throws WorkFlowException {
SimpleQuery<WorkFlowVO> query = dbf.createQuery(WorkFlowVO.class);
query.add(WorkFlowVO_.chainUuid, Op.EQ, chainvo.getUuid());
query.orderBy(WorkFlowVO_.position, Od.DESC);
List<WorkFlowVO> vos = query.list();
WorkFlowVO last = vos.get(0);
assert last.getState() == WorkFlowState.Done || last.getState() == WorkFlowState.Processing : String.format("How can work flow[%s] in %s state when restart workflow chain[uuid:%s] !!?", last.getName(), last.getState(), chainvo.getUuid());
return run(WorkFlowContext.fromBytes(last.getContext()), last);
}
protected WorkFlowContext carryOnNothing() {
logger.debug(String.format("Noting to carry on for work flow chain[uuid:%s]", chainvo.getUuid()));
if (chainvo.getState() == WorkFlowChainState.ProcessDone) {
SimpleQuery<WorkFlowVO> query = dbf.createQuery(WorkFlowVO.class);
query.add(WorkFlowVO_.chainUuid, Op.EQ, chainvo.getUuid());
query.add(WorkFlowVO_.position, Op.EQ, chainvo.getTotalWorkFlows() - 1);
WorkFlowVO vo = query.find();
return WorkFlowContext.fromBytes(vo.getContext());
} else if (chainvo.getState() == WorkFlowChainState.RollbackDone) {
return null;
} else {
assert false : "Cannot be here";
return null;
/*
SimpleQuery<WorkFlowVO> query = dbf.createQuery(WorkFlowVO.class);
query.add(WorkFlowVO_.chainUuid, Op.EQ, chainvo.getUuid());
query.add(WorkFlowVO_.position, Op.EQ, chainvo.getTotalWorkFlows() - 1);
WorkFlowVO vo = query.find();
if (vo.getState() == WorkFlowState.Done) {
chainvo.setState(WorkFlowChainState.ProcessDone);
chainvo.setCurrentPosition(chainvo.getTotalWorkFlows()-1);
chainvo = dbf.update(chainvo);
return WorkFlowContext.fromBytes(vo.getContext());
} else if (vo.getState() == WorkFlowState.RollbackDone) {
query = dbf.createQuery(WorkFlowVO.class);
query.select(WorkFlowVO_.reason);
query.add(WorkFlowVO_.chainUuid, Op.EQ, chainvo.getUuid());
query.add(WorkFlowVO_.reason, Op.NOT_NULL);
String reason = query.findValue();
chainvo.setReason(reason);
chainvo.setState(WorkFlowChainState.RollbackDone);
chainvo.setCurrentPosition(chainvo.getTotalWorkFlows()-1);
chainvo = dbf.update(chainvo);
return null;
} else {
throw new CloudRuntimeException(String.mediaType("Program error: the latest work flow[%s] is in %s state, but work flow chain[uuid:%s] is in %s state", vo.getName(), vo.getState(), chainvo.getUuid(), chainvo.getState()));
}
*/
}
}
}