fix(orchestration): Prevent workf. cancellation to execute while rescheduling (#12903)

**What**
Currently, when cancelling async workflows, the step will get rescheduled while the current worker try to continue the execution leading to concurrency failure on compensation. This pr prevent the current worker from executing while an async step gets rescheduled

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2025-07-16 16:44:09 +02:00
committed by GitHub
parent eb83954f23
commit c5d609d09c
9 changed files with 305 additions and 63 deletions

View File

@@ -3,7 +3,8 @@ export * from "./workflow_2"
export * from "./workflow_async"
export * from "./workflow_conditional_step"
export * from "./workflow_idempotent"
export * from "./workflow_not_idempotent_with_retention"
export * from "./workflow_parallel_async"
export * from "./workflow_step_timeout"
export * from "./workflow_sync"
export * from "./workflow_transaction_timeout"
export * from "./workflow_not_idempotent_with_retention"

View File

@@ -0,0 +1,76 @@
import { Modules } from "@medusajs/framework/utils"
import {
createStep,
createWorkflow,
parallelize,
StepResponse,
} from "@medusajs/framework/workflows-sdk"
const step_2 = createStep(
{
name: "step_2",
async: true,
},
async (_, { container }) => {
const we = container.resolve(Modules.WORKFLOW_ENGINE)
await we.run("workflow_sub_workflow", {
throwOnError: true,
})
}
)
const parallelStep2Invoke = jest.fn(() => {
throw new Error("Error in parallel step")
})
const step_2_sub = createStep(
{
name: "step_2",
async: true,
},
parallelStep2Invoke
)
const subFlow = createWorkflow(
{
name: "workflow_sub_workflow",
retentionTime: 1000,
},
function (input) {
step_2_sub()
}
)
const step_1 = createStep(
{
name: "step_1",
async: true,
},
jest.fn(() => {
return new StepResponse("step_1")
})
)
const parallelStep3Invoke = jest.fn(() => {
return new StepResponse({
done: true,
})
})
const step_3 = createStep(
{
name: "step_3",
async: true,
},
parallelStep3Invoke
)
createWorkflow(
{
name: "workflow_parallel_async",
retentionTime: 1000,
},
function (input) {
parallelize(step_1(), step_2(), step_3())
}
)

View File

@@ -1,3 +1,4 @@
import { MedusaContainer } from "@medusajs/framework"
import {
DistributedTransactionType,
TransactionState,
@@ -43,7 +44,6 @@ import {
workflowEventGroupIdStep2Mock,
} from "../__fixtures__/workflow_event_group_id"
import { createScheduled } from "../__fixtures__/workflow_scheduled"
import { container, MedusaContainer } from "@medusajs/framework"
jest.setTimeout(60000)
@@ -143,7 +143,7 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
})
describe("Cancel transaction", function () {
it("should cancel an ongoing execution with async unfinished yet step", async () => {
it("should cancel an ongoing execution with async unfinished yet step", (done) => {
const transactionId = "transaction-to-cancel-id"
const step1 = createStep("step1", async () => {
return new StepResponse("step1")
@@ -168,25 +168,39 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
return new WorkflowResponse("finished")
})
await workflowOrcModule.run(workflowId, {
input: {},
transactionId,
})
workflowOrcModule
.run(workflowId, {
input: {},
transactionId,
})
.then(async () => {
await setTimeoutPromise(100)
await setTimeoutPromise(100)
await workflowOrcModule.cancel(workflowId, {
transactionId,
})
await workflowOrcModule.cancel(workflowId, {
transactionId,
})
workflowOrcModule.subscribe({
workflowId,
transactionId,
subscriber: async (event) => {
if (event.eventType === "onFinish") {
const execution =
await workflowOrcModule.listWorkflowExecutions({
transaction_id: transactionId,
})
await setTimeoutPromise(1000)
expect(execution.length).toEqual(1)
expect(execution[0].state).toEqual(
TransactionState.REVERTED
)
done()
}
},
})
})
const execution = await workflowOrcModule.listWorkflowExecutions({
transaction_id: transactionId,
})
expect(execution.length).toEqual(1)
expect(execution[0].state).toEqual(TransactionState.REVERTED)
failTrap(done)
})
it("should cancel a complete execution with a sync workflow running as async", async () => {
@@ -898,7 +912,6 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
expect(spy).toHaveBeenCalledTimes(1)
console.log(spy.mock.results)
expect(spy).toHaveReturnedWith(
expect.objectContaining({ output: { testValue: "test" } })
)
@@ -944,6 +957,35 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
expect(executionsList).toHaveLength(1)
expect(executionsListAfter).toHaveLength(1)
})
it("should display error when multple async steps are running in parallel", (done) => {
void workflowOrcModule.run("workflow_parallel_async", {
input: {},
throwOnError: false,
})
void workflowOrcModule.subscribe({
workflowId: "workflow_parallel_async",
subscriber: (event) => {
if (event.eventType === "onFinish") {
done()
expect(event.errors).toEqual(
expect.arrayContaining([
expect.objectContaining({
action: "step_2",
handlerType: "invoke",
error: expect.objectContaining({
message: "Error in parallel step",
}),
}),
])
)
}
},
})
failTrap(done)
})
})
describe("Cleaner job", function () {