feat(orchestration,workflows-sdk): Skip step (#8334)

This commit is contained in:
Carlos R. L. Rodrigues
2024-07-29 14:26:47 -03:00
committed by GitHub
parent e98012a858
commit 24c105f288
17 changed files with 272 additions and 33 deletions
@@ -1207,7 +1207,7 @@ describe("Transaction Orchestrator", () => {
expect(
transaction.getFlow().steps["_root.action1.action2.action4"].invoke
.state
).toBe(TransactionStepState.SKIPPED)
).toBe(TransactionStepState.SKIPPED_FAILURE)
expect(
transaction.getFlow().steps["_root.action1.action2.action4"].invoke
.status
@@ -14,6 +14,19 @@ export class PermanentStepFailureError extends Error {
}
}
export class SkipStepResponse extends Error {
static isSkipStepResponse(error: Error): error is SkipStepResponse {
return (
error instanceof SkipStepResponse || error?.name === "SkipStepResponse"
)
}
constructor(message?: string) {
super(message)
this.name = "SkipStepResponse"
}
}
export class TransactionStepTimeoutError extends Error {
static isTransactionStepTimeoutError(
error: Error
@@ -21,6 +21,7 @@ import {
isErrorLike,
PermanentStepFailureError,
serializeError,
SkipStepResponse,
TransactionStepTimeoutError,
TransactionTimeoutError,
} from "./errors"
@@ -36,6 +37,7 @@ export type TransactionFlow = {
}
hasAsyncSteps: boolean
hasFailedSteps: boolean
hasSkippedOnFailureSteps: boolean
hasWaitingSteps: boolean
hasSkippedSteps: boolean
hasRevertedSteps: boolean
@@ -125,6 +127,7 @@ export class TransactionOrchestrator extends EventEmitter {
TransactionStepState.FAILED,
TransactionStepState.TIMEOUT,
TransactionStepState.SKIPPED,
TransactionStepState.SKIPPED_FAILURE,
]
const siblings = this.getPreviousStep(flow, previousStep).next.map(
@@ -143,6 +146,7 @@ export class TransactionOrchestrator extends EventEmitter {
TransactionStepState.REVERTED,
TransactionStepState.FAILED,
TransactionStepState.DORMANT,
TransactionStepState.SKIPPED,
]
const siblings = step.next.map((sib) => flow.steps[sib])
return (
@@ -253,6 +257,7 @@ export class TransactionOrchestrator extends EventEmitter {
completed: number
}> {
let hasSkipped = false
let hasSkippedOnFailure = false
let hasIgnoredFailure = false
let hasFailed = false
let hasWaiting = false
@@ -328,7 +333,9 @@ export class TransactionOrchestrator extends EventEmitter {
} else {
completedSteps++
if (curState.state === TransactionStepState.SKIPPED) {
if (curState.state === TransactionStepState.SKIPPED_FAILURE) {
hasSkippedOnFailure = true
} else if (curState.state === TransactionStepState.SKIPPED) {
hasSkipped = true
} else if (curState.state === TransactionStepState.REVERTED) {
hasReverted = true
@@ -358,6 +365,9 @@ export class TransactionOrchestrator extends EventEmitter {
return await this.checkAllSteps(transaction)
} else if (completedSteps === totalSteps) {
if (hasSkippedOnFailure) {
flow.hasSkippedOnFailureSteps = true
}
if (hasSkipped) {
flow.hasSkippedSteps = true
}
@@ -453,6 +463,39 @@ export class TransactionOrchestrator extends EventEmitter {
transaction.emit(eventName, { step, transaction })
}
private static async skipStep(
transaction: DistributedTransaction,
step: TransactionStep
): Promise<void> {
const hasStepTimedOut =
step.getStates().state === TransactionStepState.TIMEOUT
const flow = transaction.getFlow()
const options = TransactionOrchestrator.getWorkflowOptions(flow.modelId)
if (!hasStepTimedOut) {
step.changeStatus(TransactionStepStatus.OK)
step.changeState(TransactionStepState.SKIPPED)
}
if (step.definition.async || options?.storeExecution) {
await transaction.saveCheckpoint()
}
const cleaningUp: Promise<unknown>[] = []
if (step.hasRetryScheduled()) {
cleaningUp.push(transaction.clearRetry(step))
}
if (step.hasTimeout()) {
cleaningUp.push(transaction.clearStepTimeout(step))
}
await promiseAll(cleaningUp)
const eventName = DistributedTransactionEvent.STEP_SKIPPED
transaction.emit(eventName, { step, transaction })
}
private static async setStepTimeout(
transaction: DistributedTransaction,
step: TransactionStep,
@@ -539,7 +582,7 @@ export class TransactionOrchestrator extends EventEmitter {
) {
for (const childStep of step.next) {
const child = flow.steps[childStep]
child.changeState(TransactionStepState.SKIPPED)
child.changeState(TransactionStepState.SKIPPED_FAILURE)
}
} else {
flow.state = TransactionState.WAITING_TO_COMPENSATE
@@ -701,6 +744,12 @@ export class TransactionOrchestrator extends EventEmitter {
)
}
const output = response?.__type ? response.output : response
if (SkipStepResponse.isSkipStepResponse(output)) {
await TransactionOrchestrator.skipStep(transaction, step)
return
}
await TransactionOrchestrator.setStepSuccess(
transaction,
step,
@@ -754,11 +803,19 @@ export class TransactionOrchestrator extends EventEmitter {
)
}
await TransactionOrchestrator.setStepSuccess(
transaction,
step,
response
)
let setResponse = true
if (SkipStepResponse.isSkipStepResponse(response)) {
await TransactionOrchestrator.skipStep(transaction, step)
setResponse = false
}
if (setResponse) {
await TransactionOrchestrator.setStepSuccess(
transaction,
step,
response
)
}
await transaction.scheduleRetry(
step,
@@ -912,6 +969,7 @@ export class TransactionOrchestrator extends EventEmitter {
metadata: flowMetadata,
hasAsyncSteps: features.hasAsyncSteps,
hasFailedSteps: false,
hasSkippedOnFailureSteps: false,
hasSkippedSteps: false,
hasWaitingSteps: false,
hasRevertedSteps: false,
@@ -1176,6 +1234,41 @@ export class TransactionOrchestrator extends EventEmitter {
return [transaction, step]
}
/** Skip the execution of a specific transaction and step
* @param responseIdempotencyKey - The idempotency key for the step
* @param handler - The handler function to execute the step
* @param transaction - The current transaction. If not provided it will be loaded based on the responseIdempotencyKey
*/
public async skipStep(
responseIdempotencyKey: string,
handler?: TransactionStepHandler,
transaction?: DistributedTransaction
): Promise<DistributedTransaction> {
const [curTransaction, step] =
await TransactionOrchestrator.getTransactionAndStepFromIdempotencyKey(
responseIdempotencyKey,
handler,
transaction
)
if (step.getStates().status === TransactionStepStatus.WAITING) {
this.emit(DistributedTransactionEvent.RESUME, {
transaction: curTransaction,
})
await TransactionOrchestrator.skipStep(curTransaction, step)
await this.executeNext(curTransaction)
} else {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Cannot skip a step when status is ${step.getStates().status}`
)
}
return curTransaction
}
/** Register a step success for a specific transaction and step
* @param responseIdempotencyKey - The idempotency key for the step
* @param handler - The handler function to execute the step
@@ -93,11 +93,13 @@ export class TransactionStep {
TransactionStepState.COMPENSATING,
TransactionStepState.FAILED,
TransactionStepState.SKIPPED,
TransactionStepState.SKIPPED_FAILURE,
],
[TransactionStepState.INVOKING]: [
TransactionStepState.FAILED,
TransactionStepState.DONE,
TransactionStepState.TIMEOUT,
TransactionStepState.SKIPPED,
],
[TransactionStepState.COMPENSATING]: [
TransactionStepState.REVERTED,
@@ -24,7 +24,7 @@ export type TransactionStepsDefinition = {
/**
* Indicates whether the workflow should continue even if there is a permanent failure in this step.
* In case it is set to true, the children steps of this step will not be executed and their status will be marked as TransactionStepState.SKIPPED.
* In case it is set to true, the children steps of this step will not be executed and their status will be marked as TransactionStepState.SKIPPED_FAILURE.
*/
continueOnPermanentFailure?: boolean
@@ -164,6 +164,7 @@ export enum DistributedTransactionEvent {
TIMEOUT = "timeout",
STEP_BEGIN = "stepBegin",
STEP_SUCCESS = "stepSuccess",
STEP_SKIPPED = "stepSkipped",
STEP_FAILURE = "stepFailure",
STEP_AWAITING = "stepAwaiting",
COMPENSATE_STEP_SUCCESS = "compensateStepSuccess",
@@ -211,6 +212,11 @@ export type DistributedTransactionEvents = {
step: TransactionStep
transaction: DistributedTransaction
}) => void
onStepSkipped?: (args: {
step: TransactionStep
transaction: DistributedTransaction
}) => void
}
export type StepFeatures = {
@@ -1,12 +1,12 @@
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import {
createMedusaContainer,
isDefined,
isString,
MedusaContext,
MedusaContextType,
MedusaError,
MedusaModuleType,
createMedusaContainer,
isDefined,
isString,
} from "@medusajs/utils"
import { asValue } from "awilix"
import {
@@ -281,6 +281,13 @@ export class LocalWorkflow {
eventWrapperMap.get("onCompensateStepFailure")
)
}
if (subscribe?.onStepSkipped) {
transaction.on(
DistributedTransactionEvent.STEP_SKIPPED,
eventWrapperMap.get("onStepSkipped")
)
}
}
if (transaction) {