feat(orchestration,workflows-sdk): Skip step (#8334)
This commit is contained in:
committed by
GitHub
parent
e98012a858
commit
24c105f288
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user