UNPKG

simple-swf

Version:

Actually simple API layer for AWS SWF

330 lines (324 loc) 13.7 kB
// TODO: port this code! it is fairly well tested // in etl_engine, but too much work to port now... /* * translates the list of events grouped by state and encapsulating id (such as activity of workflow id) * first group by type * { * 'activity': ... * 'workflow': ... * 'decider': ... * 'markers': ... * 'signal':... * 'byEventId': { * 'event1': //rawEvent * ... * } * } * then state: * { * 'activity' : { * 'completed': { * taska: { * // raw event * } * }, * scheduled: { * taskb: { * //raw event * } * } * } * } * All meta data in the chain of events is merged, so if you are in completed state, it will be the scheduled, started, and completed events all merged * The group that an event is found is the current state of that event * * the two exceptions are the 'signals' and 'markers' groups as they only have a single state of events, as well as the byEventId which is just a cache for looking up events that refer to other events * * We build up this state somewhat like a state machine, which each event transitioning a task to a new state. This is rebuilt on every decision task (but could be memoized) to allow for statelessness * * } */ import * as _ from 'lodash' import { SWF } from 'aws-sdk' // short hand name for states export const states = { SCH: 'scheduled', FAS: 'failedToSchedule', ST: 'started', FA: 'failed', CO: 'completed', TO: 'timedOut', TE: 'terminate', TC: 'toCancel', CAL: 'canceling', CAD: 'canceled', CF: 'cancelFailed' } // mock logger let log = { error(...args: any[]) { } } // handlers for each event type that are 'manual', pretty much they either don't act on the state machine or are more complex than the events below let transitions = { WorkflowExecutionStarted(state, event) { return {state} }, WorkflowExecutionCancelRequested(state, event) { // move scheduled and running to cancel var types = ['activity', 'workflow'] for (var type of types) { var forType = state[type] for (var id in forType) { var group = forType[id] if (group.current === states.ST || group.current === states.SCH) { group.toCancel = _.merge(group[states.ST], group[states.SCH]) group.current = 'toCancel' } } } return {state} }, WorkflowExecutionCompleted(state, event) { return {state, wait: true, notify: true} }, CompleteWorkflowExecutionFailed(state, event) { return {state, error: 'completing the workflow failed', notify: true} }, WorkflowExecutionFailed(state, event) { return {state, error: 'workflow execution failed', notify: true} }, FailWorkflowExecutionFailed(state, event) { return {state, error: 'failed to fail execution', notify: true} }, WorkflowExecutionTimedOut(state, event) { return {state, error: 'workflow timed out', notify: true} }, WorkflowExecutionCanceled(state, event) { return {state, wait: true} }, CancelWorkflowExecutionFailed(state, event) { return {state, error: 'failed to cancel workflow', notify: true} }, WorkflowExecutionTerminated(state, event) { return {state, wait: true} }, WorkflowExecutionSignaled(state, event) { state.signals[event.workflowExecutionSignaledEventAttributes.signalName] = event return {state} } } // describes the transitions from one state space to another let describeTransition = { DecisionTaskScheduled(state, event) { return {type: 'decision', id: event.eventId, to: states.SCH} }, DecisionTaskStarted(state, event) { var eventId = event.decisionTaskStartedEventAttributes.scheduledEventId return {type: 'decision', id: eventId, to: states.ST, from: states.SCH} }, DecisionTaskCompleted(state, event) { var eventId = event.decisionTaskCompletedEventAttributes.scheduledEventId return {type: 'decision', id: eventId, to: states.CO, from: states.ST} }, DecisionTaskTimedOut(state, event) { var eventId = event.decisionTaskTimedOutEventAttributes.scheduledEventId return {type: 'decision', id: eventId, to: states.TO, from: [states.SCH, states.ST]} }, ActivityTaskScheduled(state, event) { var activityId = event.activityTaskScheduledEventAttributes.activityId return {type: 'activity', id: activityId, to: states.SCH, from: [states.FA, states.TO]} }, ScheduleActivityTaskFailed(state, event) { var activityId = event.scheduleActivityTaskFailedEventAttributes.activityId return {type: 'activity', id: activityId, to: states.FAS, error: 'failed to schedule activity ' + activityId, notify: true} }, ActivityTaskStarted(state, event) { var eventId = event.activityTaskStartedEventAttributes.scheduledEventId var activityId = state.byEventId[eventId].activityTaskScheduledEventAttributes.activityId return {type: 'activity', id: activityId, to: states.ST, from: states.SCH} }, ActivityTaskCompleted(state, event) { var eventId = event.activityTaskCompletedEventAttributes.scheduledEventId var activityId = state.byEventId[eventId].activityTaskScheduledEventAttributes.activityId function addResult(group, e) { var res = e.activityTaskCompletedEventAttributes.result try { res = JSON.parse(res) } catch (e) { } group.result = res } return {type: 'activity', id: activityId, to: states.CO, from: states.ST, transform: addResult} }, ActivityTaskFailed(state, event) { var eventId = event.activityTaskFailedEventAttributes.scheduledEventId var activityId = state.byEventId[eventId].activityTaskScheduledEventAttributes.activityId return {type: 'activity', id: activityId, to: states.FA, from: states.ST} }, ActivityTaskTimedOut(state, event) { var eventId = event.activityTaskTimedOutEventAttributes.scheduledEventId var activityId = state.byEventId[eventId].activityTaskScheduledEventAttributes.activityId return {type: 'activity', id: activityId, to: states.TO, from: [states.ST, states.SCH]} }, ActivityTaskCanceled(state, event) { var eventId = event.activityTaskCanceledEventAttributes.scheduledEventId var activityId = state.byEventId[eventId].activityTaskScheduledEventAttributes.activityId return {type: 'activity', id: activityId, to: states.CAD, from: states.CAL} }, ActivityTaskCancelRequested(state, event) { var activityId = event.activityTaskCancelRequestedEventAttributes.activityId return {type: 'activity', id: activityId, to: states.CAL, from: [states.TC, states.SCH, states.ST]} }, RequestCancelActivityTaskFailed(state, event) { var activityId = event.requestCancelActivityTaskFailedEventAttributes.activityId return {type: 'activity', id: activityId, to: states.CF, from: states.CAL, error: 'failed to cancel request', notify: true} }, LambdaFunctionScheduled(state, event) { var activityId = event.lambdaFunctionScheduledEventAttributes.id return {type: 'activity', id: activityId, to: states.SCH, from: [states.FA, states.TO]} }, ScheduleLambdaFunctionFailed(state, event) { var activityId = event.scheduleLambdaFunctionFailedEventAttributes.id return {type: 'activity', id: activityId, to: states.FAS, error: 'failed to schedule lambda ' + activityId, notify: true} }, LambdaFunctionStarted(state, event) { var eventId = event.lambdaFunctionStartedEventAttributes.scheduledEventId var activityId = state.byEventId[eventId].lambdaFunctionScheduledEventAttributes.activityId return {type: 'activity', id: activityId, to: states.ST, from: states.SCH} }, LambdaFunctionCompleted(state, event) { var eventId = event.lambdaFunctionCompletedEventAttributes.scheduledEventId var activityId = state.byEventId[eventId].scheduleLambdaFunctionFailedEventAttributes.activityId function addResult(group, e) { var res = e.lambdaFunctionCompletedEventAttributes.result try { res = JSON.parse(res) } catch (e) { } group.result = res } return {type: 'activity', id: activityId, to: states.CO, from: states.ST, transform: addResult} }, LambdaFunctionFailed(state, event) { var eventId = event.lambdaFunctionFailedEventAttributes.scheduledEventId var activityId = state.byEventId[eventId].activityTaskScheduledEventAttributes.activityId return {type: 'activity', id: activityId, to: states.FA, from: states.ST} }, LambdaFunctionTimedOut(state, event) { var eventId = event.activityTaskTimedOutEventAttributes.scheduledEventId var activityId = state.byEventId[eventId].activityTaskScheduledEventAttributes.activityId return {type: 'activity', id: activityId, to: states.TO, from: [states.ST, states.SCH]} }, StartLambdaFunctionFailed(state, event) { var eventId = event.activityTaskCanceledEventAttributes.scheduledEventId var activityId = state.byEventId[eventId].activityTaskScheduledEventAttributes.activityId return {type: 'activity', id: activityId, to: states.FAS, from: states.ST} }, StartChildWorkflowExecutionInitiated(state, event) { var workflowId = event.startChildWorkflowExecutionInitiatedEventAttributes.workflowId return {type: 'workflow', id: workflowId, to: states.SCH} }, StartChildWorkflowExecutionFailed(state, event) { var workflowId = event.startChildWorkflowExecutionFailedEventAttributes.workflowId return {type: 'workflow', id: workflowId, to: states.FAS, from: states.SCH, error: 'failed to start child workflow', notify: true} }, ChildWorkflowExecutionStarted(state, event) { var workflowId = event.childWorkflowExecutionStartedEventAttributes.workflowExecution.workflowId return {type: 'workflow', id: workflowId, to: states.ST, from: states.SCH, states: [states.FA, states.TO]} }, ChildWorkflowExecutionCompleted(state, event) { var workflowId = event.childWorkflowExecutionCompletedEventAttributes.workflowExecution.workflowId function addResult(group, e) { var res = e.childWorkflowExecutionCompletedEventAttributes.result try { res = JSON.parse(res) } catch (e) { } group.result = res } return {type: 'workflow', id: workflowId, to: states.CO, from: states.ST, transform: addResult} }, ChildWorkflowExecutionFailed(state, event) { var workflowId = event.childWorkflowExecutionFailedEventAttributes.workflowExecution.workflowId return {type: 'workflow', id: workflowId, to: states.FA, from: states.ST} }, ChildWorkflowExecutionTimedOut(state, event) { var workflowId = event.childWorkflowExecutionTimedOutEventAttributes.workflowExecution.workflowId return {type: 'workflow', id: workflowId, to: states.TO, from: [states.SCH, states.ST]} }, ChildWorkflowExecutionCanceled(state, event) { var workflowId = event.childWorkflowExecutionCanceledEventAttributes.workflowExecution.workflowId return {type: 'workflow', id: workflowId, to: states.CAD, from: [states.SCH, states.ST]} }, ChildWorkflowExecutionTerminated(state, event) { var workflowId = event.childWorkflowExecutionTerminatedEventAttributes.workflowExecution.workflowId return {type: 'workflow', id: workflowId, to: states.TE, from: [states.SCH, states.ST]} }, MarkerRecorded(state, event) { var markerName = event.markerRecordedEventAttributes.markerName return {type: 'marker', id: markerName, to: states.CO} }, RecordMarkerFailed(state, event) { var markerName = event.recordMarkerFailedEventAttributes.markerName return {type: 'marker', id: markerName, to: states.FAS, error: 'failed to create marker', notify: true} } } export function processEvents(events) { var state = { activity: {} as {[id: string]: any}, workflow: {} as {[id: string]: any}, decision: {} as {[id: string]: any}, marker: {} as {[id: string]: any}, byEventId: {} as {[id: string]: any}, signals: {} as {[id: string]: any}, completed: [] as any[] } events.forEach((event) => { state.byEventId[event.eventId] = event if (describeTransition[event.eventType]) { let transition = describeTransition[event.eventType](state, event) if (transition.error && transition.notify) { log.error(transition.error) } if (!transition.type || !transition.id || !transition.to) { log.error('invalid transition given', transition) throw new Error('invalid transition') } var typeState = state[transition.type] || {} var grouped = typeState[transition.id] || {} grouped.current = transition.to var oldEvent: any = null if (Array.isArray(transition.from)) { for (var from of transition.from) { if (grouped[from]) { oldEvent = grouped[from] break } oldEvent = {} } } else { oldEvent = grouped[transition.from] || {} } grouped[transition.to] = _.merge(_.clone(oldEvent), event) // allow the transitions to transform the state to add other properties we may want if (transition.transform) { transition.transform(grouped, event) } if (transition.to === states.CO) { state.completed.push({type: transition.type, id: transition.id, state: grouped}) } typeState[transition.id] = grouped state[transition.type] = typeState } else if (transitions[event.eventType]) { let transition = transitions[event.eventType](state, event) if (transition.error && transition.notify) { log.error(transition.error) } state = transition.state } else { log.error('unsupported transition') } }) return state }