feat(orchestration): skip on permanent failure (#12027)

What:
 - Added step config `skipOnPermanentFailure`. Skip all the next steps when the current step fails. If a string is used, the workflow will resume from the given step.
 - Fix `continueOnPermanentFailure` to continue the execution of the flow when a step fails.
 
```ts
createWorkflow("some-workflow", () => {
  errorStep().config({
    skipOnPermanentFailure: true,
  })
  nextStep1() // skipped
  nextStep2() // skipped
})


createWorkflow("some-workflow", () => {
  errorStep().config({
    skipOnPermanentFailure: "resume-from-here",
  });
  nextStep1(); // skipped
  nextStep2(); // skipped
  nextStep3().config({ name: "resume-from-here" }); // executed
  nextStep4(); // executed
});
```
This commit is contained in:
Carlos R. L. Rodrigues
2025-04-17 09:49:58 -03:00
committed by GitHub
parent 1c5e82af51
commit e180253d60
7 changed files with 219 additions and 18 deletions

View File

@@ -1246,7 +1246,7 @@ describe("Transaction Orchestrator", () => {
expect(transaction.getState()).toBe(TransactionState.REVERTED)
})
it("should continue the transaction and skip children steps when the Transaction Step Timeout is reached but the step is set to 'continueOnPermanentFailure'", async () => {
it("should continue the transaction and skip children steps when the Transaction Step Timeout is reached but the step is set to 'skipOnPermanentFailure'", async () => {
const mocks = {
f1: jest.fn(() => {
return "content f1"
@@ -1313,7 +1313,7 @@ describe("Transaction Orchestrator", () => {
{
timeout: 0.1, // 100ms
action: "action2",
continueOnPermanentFailure: true,
skipOnPermanentFailure: true,
next: {
action: "action4",
},

View File

@@ -21,6 +21,7 @@ import {
isDefined,
isErrorLike,
isObject,
isString,
MedusaError,
promiseAll,
serializeError,
@@ -437,7 +438,10 @@ export class TransactionOrchestrator extends EventEmitter {
} else if (curState.state === TransactionStepState.REVERTED) {
hasReverted = true
} else if (curState.state === TransactionStepState.FAILED) {
if (stepDef.definition.continueOnPermanentFailure) {
if (
stepDef.definition.continueOnPermanentFailure ||
stepDef.definition.skipOnPermanentFailure
) {
hasIgnoredFailure = true
} else {
hasFailed = true
@@ -696,12 +700,28 @@ export class TransactionOrchestrator extends EventEmitter {
if (!step.isCompensating()) {
if (
step.definition.continueOnPermanentFailure &&
(step.definition.continueOnPermanentFailure ||
step.definition.skipOnPermanentFailure) &&
!TransactionTimeoutError.isTransactionTimeoutError(timeoutError!)
) {
for (const childStep of step.next) {
const child = flow.steps[childStep]
child.changeState(TransactionStepState.SKIPPED_FAILURE)
if (step.definition.skipOnPermanentFailure) {
const until = isString(step.definition.skipOnPermanentFailure)
? step.definition.skipOnPermanentFailure
: undefined
let stepsToSkip: string[] = [...step.next]
while (stepsToSkip.length > 0) {
const currentStep = flow.steps[stepsToSkip.shift()!]
if (until && currentStep.definition.action === until) {
break
}
currentStep.changeState(TransactionStepState.SKIPPED_FAILURE)
if (currentStep.next?.length > 0) {
stepsToSkip = stepsToSkip.concat(currentStep.next)
}
}
}
} else {
flow.state = TransactionState.WAITING_TO_COMPENSATE
@@ -1351,7 +1371,7 @@ export class TransactionOrchestrator extends EventEmitter {
states[parent].next?.push(id)
}
const definitionCopy = { ...obj }
const definitionCopy = { ...obj } as TransactionStepsDefinition
delete definitionCopy.next
if (definitionCopy.async) {

View File

@@ -25,10 +25,17 @@ 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_FAILURE.
* In case it is set to true, the the current step will be marked as TransactionStepState.PERMANENT_FAILURE and the next steps will be executed.
*/
continueOnPermanentFailure?: boolean
/**
* Indicates whether the workflow should skip all subsequent steps in case of a permanent failure in this step.
* In case it is set to true, the next steps of the workflow will not be executed and their status will be marked as TransactionStepState.SKIPPED_FAILURE.
* In case it is a string, the next steps until the step name provided will be skipped and the workflow will be resumed from the step provided.
*/
skipOnPermanentFailure?: boolean | string
/**
* If true, no compensation action will be triggered for this step in case of a failure.
*/