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:
committed by
GitHub
parent
eb83954f23
commit
c5d609d09c
@@ -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"
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
)
|
||||
@@ -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 () {
|
||||
|
||||
Reference in New Issue
Block a user