@wmfs/statebox
Version:
Orchestrate Node functions using Amazon States Language
266 lines (222 loc) • 8.3 kB
JavaScript
'use strict'
// Amazon States Language reference
// Specification: https://states-language.net/spec.html
// API: http://docs.aws.amazon.com/step-functions/latest/apireference/API_CreatestateMachine.html
// https://aws.amazon.com/step-functions/
// https://aws.amazon.com/blogs/aws/new-aws-step-functions-build-distributed-applications-using-visual-workstateMachines/
const executioner = require('./executioner')
const StateMachines = require('./state-machines')
const Resources = require('./resources')
const MemoryDao = require('./dao/Memory-dao')
const StorageDao = require('./dao/StorageService-dao')
const Status = require('./Status')
const CallbackManager = require('./Callback-manager')
const debug = require('debug')('statebox')
class Statebox {
constructor (options = {}) {
this.options = options
this.ready_ = this._findDao(options)
.then(dao => {
info(options.messages, 'Statebox is ready')
this.options.dao = dao
this.options.executioner = executioner
this.options.callbackManager = new CallbackManager()
})
}
get ready () {
return this.ready_
} // ready
async _findDao (options) {
if (options.dao) {
debug('Using custom Dao')
return options.dao // custom DAO provided
}
const storageDao = await this._daoFromStorage(options)
if (storageDao) {
debug('Using Storage Dao')
return storageDao
}
debug('Using built in MemoryDao')
return new MemoryDao(options)
} // _findDao
async _daoFromStorage (options) {
if (!options.bootedServices || !options.bootedServices.storage) {
return null
}
try {
const storage = options.bootedServices.storage
info(options.messages, `Using storage service ${storage.storageName}`)
let model = storage.models[StorageDao.ExecutionModelName]
if (!model) {
info(options.messages, `Adding model ${StorageDao.ExecutionModelName}`)
model = await storage.addModel(
StorageDao.ExecutionModelName,
StorageDao.ExecutionModelDefinition,
options.messages
)
}
return new StorageDao(model)
} catch (err) {
warning(options.messages, 'Could not get Dao from storage service')
warning(options.messages, err)
warning(options.messages, 'Falling back to in-memory Dao')
}
return null
} // _daoFromStorage
createModuleResource (moduleName, moduleClass) {
Resources.createModule(moduleName, moduleClass)
}
createModuleResources (moduleResources) {
Resources.createModules(moduleResources)
}
listModuleResources () {
return Resources.listModules()
}
findModuleByName (name) {
return Resources.findModuleByName(name)
}
registerResourceResolver (resourceType, resolver) {
Resources.registerResolver(resourceType, resolver)
}
createStateMachine (stateMachineName, stateMachineDefinition, stateMachineMeta, env) {
return StateMachines.createStateMachine(
stateMachineName,
stateMachineDefinition,
stateMachineMeta,
env,
this.options)
} // createStateMachine
createStateMachines (stateMachineDefinitions, env) {
return StateMachines.createStateMachines(
stateMachineDefinitions,
env,
this.options)
} // createStateMachines
deleteStateMachine (name) {
StateMachines.deleteStateMachine(name)
}
describeStateMachine (name) {
StateMachines.describeStateMachine(name)
}
listStateMachines () {
return StateMachines.listStateMachines()
}
findStateMachineByName (name) {
return StateMachines.findStateMachineByName(name)
}
findStateMachines (options) {
return StateMachines.findStateMachines(options)
}
findStates (options) {
return StateMachines.findStates(options)
}
startExecution (input, stateMachineName, executionOptions) {
return executioner(
input,
stateMachineName,
executionOptions,
this.options
)
} // startExecution
async stopExecution (cause, errorCode, executionName, executionOptions) {
const executionDescription = await this.options.dao.findExecutionByName(executionName)
if (executionDescription && executionDescription.status === Status.RUNNING) {
return this.options.dao.stopExecution(
cause,
errorCode,
executionName,
executionOptions
)
} else {
throw new Error(`Execution is not running, and cannot be stopped (executionName='${executionName}')`)
}
} // _stopExecution
describeExecution (executionName) {
return this.options.dao.findExecutionByName(executionName)
} // describeExecution
async sendTaskSuccess (executionName, output) {
const executionDescription = await this.options.dao.findExecutionByName(executionName)
if (executionDescription && executionDescription.status === Status.RUNNING) {
const stateToRun = StateMachines.findState(
executionDescription.stateMachineName,
executionDescription.currentStateName
)
return stateToRun.runTaskSuccess(executionDescription, output)
} else {
throw new Error(`Success has been rejected because execution is not running (executionName='${executionName}')`)
}
} // sendTaskSuccess
async sendTaskFailure (executionName, errorInfo) {
const executionDescription = await this.options.dao.findExecutionByName(executionName)
if (executionDescription && executionDescription.status === Status.RUNNING) {
const stateToRun = StateMachines.findState(
executionDescription.stateMachineName,
executionDescription.currentStateName
)
return stateToRun.runTaskFailure(executionDescription, errorInfo)
} else {
throw new Error(`Failure has been rejected because execution is not running (executionName='${executionName}')`)
}
} // sendTaskFailure
sendTaskHeartbeat (executionName, updateResult) {
return this.sendHeartbeat_(executionName, updateResult, false)
} // _sendTaskHeartbeat
sendTaskLastHeartbeat (executionName, updateResult) {
return this.sendHeartbeat_(executionName, updateResult, true)
} // sendTaskLastHeartbeat
async sendHeartbeat_ (executionName, updateResult, isLast) {
const executionDescription = await this.options.dao.findExecutionByName(executionName)
if (executionDescription && executionDescription.status === Status.RUNNING) {
const stateToRun = StateMachines.findState(
executionDescription.stateMachineName,
executionDescription.currentStateName
)
return stateToRun.runTaskHeartbeat(executionDescription, updateResult, isLast)
} else {
throw new Error(`Heartbeat has been rejected because execution is not running (executionName='${executionName}')`)
}
}
async sendTaskRevivification (executionName) {
const executionDescription = await this.options.dao.findExecutionByName(executionName)
if (executionDescription && executionDescription.status === Status.FAILED) {
const stateMachine = StateMachines.findStateMachineByName(executionDescription.stateMachineName)
return stateMachine.runState(executionDescription)
} else {
throw new Error(`Revivification has been rejected because execution is not failed (executionName='${executionName}')`)
}
}
async waitUntilStoppedRunning (executionName) {
let notFound = 0
do {
const executionDescription = await this.options.dao.findExecutionByName(executionName)
if (typeof executionDescription !== 'object') {
++notFound
} else if (executionDescription.status !== Status.RUNNING) {
return executionDescription
}
await pause(50)
} while (notFound !== 5)
throw new Error(`Could not find execution ${executionName}`)
} // _waitUntilStoppedRunning
} // class Statebox
function pause (duration) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(), duration)
})
} // _pause
function info (messages, msg) {
if (messages) {
messages.info(msg)
} else {
console.log(msg)
}
} // info
function warning (messages, msg) {
if (messages) {
info(messages, msg)
messages.warning(msg)
} else {
console.log(msg)
}
} // warning
module.exports = Statebox