UNPKG

@wmfs/statebox

Version:

Orchestrate Node functions using Amazon States Language

260 lines (211 loc) 8.5 kB
const debugPackage = require('debug')('statebox') const StateMachines = require('./../../state-machines') const _ = require('lodash') const Status = require('../../Status') const States = require('./errors') const PathHandlers = require('./path-handlers') class BaseState { constructor (stateName, stateMachine, stateDefinition, options) { this.name = stateName this.stateMachine = stateMachine this.stateMachineName = stateMachine.name this.definition = stateDefinition this.dao = options.dao this.callbackManager = options.callbackManager this.inputSelector = PathHandlers.Input(stateDefinition.InputPath, stateDefinition.Parameters) this.applyResult = PathHandlers.Result(stateDefinition.ResultPath) this.outputSelector = PathHandlers.Output(stateDefinition.OutputPath) } // constructor initialise (env) { if (!this.stateTypeInit) { return } return this.stateTypeInit(env) } // initialise debug () { debugPackage(` - Created '${this.name}' ${this.stateType} within stateMachine '${this.stateMachine.name}'`) } // debug run (executionDescription, optionalDoneCallback) { try { const input = this.inputSelector(executionDescription.ctx) return this.process(executionDescription, input, optionalDoneCallback) } catch (e) { return this.processTaskFailure(e, executionDescription.executionName) } } // run // State Failure async processTaskFailure (errorInfo, executionName) { const executionDescription = await this.dao.findExecutionByName( executionName ) return this.runTaskFailure( executionDescription, errorInfo ) } // processTaskFailure async runTaskFailure (executionDescription, errorInfo) { const executionName = executionDescription.executionName const stateDefinition = StateMachines.findStateDefinition( executionDescription.stateMachineName, executionDescription.currentStateName ) // Error Recovery const catchHandler = this.hasCatchHandler(stateDefinition, errorInfo.error) if (catchHandler) { return this.runTaskNextState( executionDescription, catchHandler, executionDescription.ctx ) } // Everything's trashed const failedExecutionDescription = await this.dao.failExecution( executionName, errorInfo ) this.handleChildExecutionFail(executionDescription, errorInfo) this.fireExecutionCompleted( executionName, failedExecutionDescription ) return failedExecutionDescription } // runTaskFailure handleChildExecutionFail (executionDescription, errorInfo) { const parentExecutionName = executionDescription.executionOptions.parentExecutionName if (!parentExecutionName) return debugPackage(`Branch ${executionDescription.executionName} failed in ${parentExecutionName}`) this.processTaskFailure( States.BranchFailed.error( errorInfo.cause || 'Failed because a state in a parallel branch has failed' ), parentExecutionName ) } // handleChildExecutionFail hasCatchHandler (stateDefinition, errorRaised) { if (!stateDefinition.Catch) { return null } for (const catchBlock of stateDefinition.Catch) { for (const exceptionName of catchBlock.ErrorEquals) { if (exceptionName === errorRaised || exceptionName === 'States.ALL') { return catchBlock.Next } } // for ... } // for ... return null } // hasCatchHandler // State Heartbeat async processTaskHeartbeat (output, executionName) { const executionDescription = await this.dao.findExecutionByName( executionName ) return this.runTaskHeartbeat( executionDescription, output ) } // processTaskHeartbeat async runTaskHeartbeat (executionDescription, updateResult, isLast = false) { const executionName = executionDescription.executionName const intermediateResult = this.applyResult(executionDescription.ctx, updateResult) const output = this.outputSelector(intermediateResult) executionDescription.ctx = _.defaultsDeep(output, executionDescription.ctx) await this.dao.setNextState( executionName, // executionName executionDescription.currentStateName, // nextStateName executionDescription.currentResource, // nextResource executionDescription.ctx ) if (isLast) { // this was the last heartbeat, so advance to the next stage this.runTaskSuccess(executionDescription, executionName.ctx) } return executionDescription } // runTaskHeartbeat // Task Success async processTaskSuccess (executionName, output) { const executionDescription = await this.dao.findExecutionByName(executionName) // TODO: Handle failure as per spec! return this.runTaskSuccess(executionDescription, output) } // processTaskSuccess runTaskSuccess (executionDescription, result) { const executionName = executionDescription.executionName const intermediateResult = this.applyResult(executionDescription.ctx, result) const output = this.outputSelector(intermediateResult) const stateDefinition = StateMachines.findStateDefinition( executionDescription.stateMachineName, executionDescription.currentStateName ) // END if (stateDefinition.End || stateDefinition.Type === 'Succeed' || stateDefinition.Type === 'Fail') { return this.runTaskEnd(executionName, executionDescription, output) } else { // NEXT return this.runTaskNextState(executionDescription, stateDefinition.Next, output) } } // runTaskSuccess async runTaskEnd (executionName, executionDescription, ctx) { const succeededExecutionDescription = await this.dao.succeedExecution( executionName, ctx ) this.handleChildExecutionSuccess(executionDescription) this.fireExecutionCompleted( executionName, succeededExecutionDescription ) return succeededExecutionDescription } // runTaskEnd async handleChildExecutionSuccess (executionDescription) { const parentExecutionName = executionDescription.executionOptions.parentExecutionName if (!parentExecutionName) return const allChildren = await this.dao.findExecutionsByParentName(parentExecutionName) const completedCount = allChildren.filter(child => child.status !== Status.RUNNING).length const failedCount = allChildren.filter(child => child.status === Status.FAILED).length const parentCompleted = (completedCount === allChildren.length) const parentSucceeded = parentCompleted && (failedCount === 0) if (parentCompleted) { debugPackage(`All branches have now succeeded in ${parentExecutionName}`) const parentCtx = makeParentContext(allChildren) const parentState = StateMachines.findState( executionDescription.executionOptions.parentStateMachineName, executionDescription.executionOptions.parentStateName ) return parentState.processTaskSuccess(parentExecutionName, parentCtx) } else if (parentSucceeded) { debugPackage(`All branches have now completed in ${parentExecutionName} with ${failedCount} failures`) } else { debugPackage(`${completedCount} of ${allChildren.length} completed in ${parentExecutionName}`) } } // handleChildExecutionSuccess async runTaskNextState (executionDescription, nextStateName, ctx) { const executionName = executionDescription.executionName const stateMachine = StateMachines.findStateMachineByName(executionDescription.stateMachineName) const nextDefinition = stateMachine.findStateDefinitionByName(nextStateName) const nextResource = nextDefinition.Resource await this.dao.setNextState( executionName, nextStateName, // nextStateName nextResource, ctx ) // TODO: Error needs handling as per spec return stateMachine.processState(executionName) } // runTaskNextState fireExecutionCompleted (executionName, executionDescription) { this.callbackManager.fireCallback( Status.COMPLETE, executionName, executionDescription ) } // fireExecutionComplete } // class BaseState function makeParentContext (allChildren) { const ctx = [] for (const child of allChildren) { const branchIndex = child.executionOptions.branchIndex const childCtx = child.ctx ctx[branchIndex] = childCtx } return ctx } module.exports = BaseState