fix(): Workflow cancellation + gracefully handle non serializable state (#10674)
FIXES FRMW-2852 **What** A workflow distributed transaction expect any response and error to be serializable. When it is not the case, the distributed transaction might fail during the save checkpoint that occurs for async steps. This can lead to unexpected behaviour. With this pr, we introduce a way to handle non serialazable object in a more sustainable manner, this means the following: - If a workflow throw any non serialazable error (e.g AWS error that contains full IncomingMessage object that related to network communication, think of req/res) then we identify that this object is not serialzable and we clean up the object to make it serializable without loosing the main information, add a new error to the workflow to informed of this issue and can be handled by the user. - If a response is not serializable (which should not happen at this point because it is handled before by the value resolver), in that case, we wont be able to reuse that response to continue the workflow which means that the workflow is in a non runnable state. In that case we throw a specific error stating that a non serializable context is being provided **second what** This pr refactor the `runAsStep` to add better support for workflow cancelation, especially async ones
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { TransactionState } from "@medusajs/utils"
|
||||
import { createStep } from "../create-step"
|
||||
import { createWorkflow } from "../create-workflow"
|
||||
import { StepResponse } from "../helpers"
|
||||
@@ -42,6 +43,44 @@ describe("Workflow composer", () => {
|
||||
expect(result).toEqual({ result: "hi from outside" })
|
||||
})
|
||||
|
||||
it("should cancel transaction on failed sub workflow call", async function () {
|
||||
const step1 = createStep("step1", async (_, context) => {
|
||||
return new StepResponse("step1")
|
||||
})
|
||||
|
||||
const step2 = createStep("step2", async (input: string, context) => {
|
||||
return new StepResponse({ result: input })
|
||||
})
|
||||
const step3 = createStep("step3", async (input: string, context) => {
|
||||
throw new Error("I have failed")
|
||||
})
|
||||
|
||||
const subWorkflow = createWorkflow(
|
||||
getNewWorkflowId(),
|
||||
function (input: WorkflowData<string>) {
|
||||
step1()
|
||||
return new WorkflowResponse(step2(input))
|
||||
}
|
||||
)
|
||||
|
||||
const workflow = createWorkflow(getNewWorkflowId(), function () {
|
||||
const subWorkflowRes = subWorkflow.runAsStep({
|
||||
input: "hi from outside",
|
||||
})
|
||||
return new WorkflowResponse(step3(subWorkflowRes.result))
|
||||
})
|
||||
|
||||
const { errors, transaction } = await workflow.run({
|
||||
input: {},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
expect(errors).toHaveLength(1)
|
||||
expect(errors[0].error.message).toEqual("I have failed")
|
||||
|
||||
expect(transaction.getState()).toEqual(TransactionState.REVERTED)
|
||||
})
|
||||
|
||||
it("should skip step if condition is false", async function () {
|
||||
const step1 = createStep("step1", async (_, context) => {
|
||||
return new StepResponse({ result: "step1" })
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
OrchestrationUtils,
|
||||
} from "@medusajs/utils"
|
||||
import { ulid } from "ulid"
|
||||
import { exportWorkflow } from "../../helper"
|
||||
import { exportWorkflow, WorkflowResult } from "../../helper"
|
||||
import { createStep } from "./create-step"
|
||||
import { proxify } from "./helpers/proxy"
|
||||
import { StepResponse } from "./helpers/step-response"
|
||||
@@ -201,20 +201,29 @@ export function createWorkflow<TData, TResult, THooks extends any[]>(
|
||||
},
|
||||
})
|
||||
|
||||
const { result, transaction: flowTransaction } = transaction
|
||||
const { result } = transaction
|
||||
|
||||
if (!context.isAsync || flowTransaction.hasFinished()) {
|
||||
return new StepResponse(result, transaction)
|
||||
}
|
||||
|
||||
return
|
||||
return new StepResponse(
|
||||
result,
|
||||
context.isAsync ? stepContext.transactionId : transaction
|
||||
)
|
||||
},
|
||||
async (transaction, { container }) => {
|
||||
async (transaction, stepContext) => {
|
||||
if (!transaction) {
|
||||
return
|
||||
}
|
||||
|
||||
await workflow(container).cancel(transaction)
|
||||
const { container, ...sharedContext } = stepContext
|
||||
|
||||
await workflow(container).cancel({
|
||||
transaction: (transaction as WorkflowResult<any>).transaction,
|
||||
transactionId: isString(transaction) ? transaction : undefined,
|
||||
container,
|
||||
context: {
|
||||
...sharedContext,
|
||||
parentStepIdempotencyKey: stepContext.idempotencyKey,
|
||||
},
|
||||
})
|
||||
}
|
||||
)(input) as ReturnType<StepFunction<TData, TResult>>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user