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

@@ -12,6 +12,7 @@ import {
promiseAll,
} from "@medusajs/utils"
import { asValue } from "awilix"
import { setTimeout } from "timers/promises"
import {
createStep,
createWorkflow,
@@ -24,7 +25,6 @@ import {
} from ".."
import { MedusaWorkflow } from "../../../medusa-workflow"
import { createHook } from "../create-hook"
import { setTimeout } from "timers/promises"
jest.setTimeout(30000)
@@ -1297,6 +1297,69 @@ describe("Workflow composer", function () {
})
})
it("should skip all steps in case of permanent failure", async () => {
const logStepFn = jest.fn(async ({ input }: { input: object }) => {
return new StepResponse("done")
})
const errorStep = createStep("perma-fail-step", async () => {
return StepResponse.permanentFailure("FAIL")
})
const logStep = createStep("log-step", logStepFn)
const fakeStepWorkflow = createWorkflow("fake-workflow", () => {
const result = errorStep().config({
skipOnPermanentFailure: true,
})
logStep({ input: { A: "123" } })
logStep({ input: { A: "123 a" } }).config({ name: "other" })
logStep({ input: { A: "123 b" } }).config({ name: "other_2" })
logStep({ input: { A: "123 c" } }).config({ name: "other_3" })
return new WorkflowResponse(result)
})
const { transaction } = await fakeStepWorkflow().run({
input: 1,
})
expect(transaction.getState()).toEqual("done")
expect(logStepFn).toHaveBeenCalledTimes(0)
})
it("should skip steps until the named step in case of permanent failure", async () => {
const logStepFn = jest.fn(async ({ input }: { input: object }) => {
return new StepResponse("done and returned")
})
const errorStep = createStep("perma-fail-step", async () => {
return StepResponse.permanentFailure("FAIL")
})
const logStep = createStep("log-step", logStepFn)
const fakeStepWorkflow = createWorkflow("fake-workflow", () => {
errorStep().config({
skipOnPermanentFailure: "other_2",
})
logStep({ input: { A: "123" } })
logStep({ input: { A: "123 a" } }).config({ name: "other" })
logStep({ input: { A: "123 b" } }).config({ name: "other_2" })
const ret = logStep({ input: { A: "123 c" } }).config({
name: "other_3",
})
return new WorkflowResponse(ret)
})
const { result, transaction } = await fakeStepWorkflow().run({
input: 1,
})
expect(transaction.getState()).toEqual("done")
expect(result).toEqual("done and returned")
expect(logStepFn).toHaveBeenCalledTimes(2)
})
it("should compose a new workflow and skip steps depending on the input", async () => {
const mockStep1Fn = jest.fn().mockImplementation((input) => {
if (input === 1) {
@@ -2701,4 +2764,98 @@ describe("Workflow composer", function () {
expect(workflowResult).toEqual("return from 1")
})
it("should compose a new workflow that skips steps on permanent failure [1]", async () => {
const mockStep1Fn = jest.fn().mockImplementation(async () => {
throw new Error("failed")
}) as any
const mockStep2Fn = jest.fn().mockImplementation(() => {
return new StepResponse(true)
}) as any
const mockStep3Fn = jest.fn().mockImplementation(() => {
return new StepResponse(true)
}) as any
const mockStep4Fn = jest.fn().mockImplementation(() => {
return new StepResponse(true)
}) as any
const step1 = createStep(
{
name: "step1",
skipOnPermanentFailure: "step3",
},
mockStep1Fn
)
const step2 = createStep("step2", mockStep2Fn)
const step3 = createStep("step3", mockStep3Fn)
const step4 = createStep("step4", mockStep4Fn)
const workflow = createWorkflow("workflow1", function (input) {
const ret1 = step1()
const [ret2, ret3] = parallelize(step2(), step3())
const ret4 = step4()
return new WorkflowResponse({ ret1, ret2, ret3, ret4 })
})
const { result: workflowResult } = await workflow().run()
expect(mockStep1Fn).toHaveBeenCalledTimes(1)
expect(mockStep2Fn).toHaveBeenCalledTimes(0)
expect(mockStep3Fn).toHaveBeenCalledTimes(1)
expect(mockStep4Fn).toHaveBeenCalledTimes(1)
expect(workflowResult).toEqual({
ret1: undefined,
ret2: undefined,
ret3: true,
ret4: true,
})
})
it("should compose a new workflow that skips steps on permanent failure [2]", async () => {
const mockStep1Fn = jest.fn().mockImplementation(async () => {
throw new Error("failed")
}) as any
const mockStep2Fn = jest.fn().mockImplementation(() => {
return new StepResponse(true)
}) as any
const mockStep3Fn = jest.fn().mockImplementation(() => {
return new StepResponse(true)
}) as any
const mockStep4Fn = jest.fn().mockImplementation(() => {
return new StepResponse(true)
}) as any
const step1 = createStep(
{
name: "step1",
skipOnPermanentFailure: "step4",
},
mockStep1Fn
)
const step2 = createStep("step2", mockStep2Fn)
const step3 = createStep("step3", mockStep3Fn)
const step4 = createStep("step4", mockStep4Fn)
const workflow = createWorkflow("workflow1", function (input) {
const ret1 = step1()
const [ret2, ret3] = parallelize(step2(), step3())
const ret4 = step4()
return new WorkflowResponse({ ret1, ret2, ret3, ret4 })
})
const { result: workflowResult } = await workflow().run()
expect(mockStep1Fn).toHaveBeenCalledTimes(1)
expect(mockStep2Fn).toHaveBeenCalledTimes(0)
expect(mockStep3Fn).toHaveBeenCalledTimes(0)
expect(mockStep4Fn).toHaveBeenCalledTimes(1)
expect(workflowResult).toEqual({
ret1: undefined,
ret2: undefined,
ret3: undefined,
ret4: true,
})
})
})