chore(): only execute race execution checks and publish message only for async workflows (#13396)

* chore(): only execute race execution checks for async workflows

* chore(): workflow redis publish only for async flows

* Create cyan-gorillas-poke.md

* chore(): workflow redis publish only for async flows

* fix negative check

---------

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
Adrien de PerettiandCarlos R. L. Rodrigues authored and GitHub committed 2025-09-04 09:12:09 +02:00
1 parent 28830567e7
commit 2b89510df3
7 files changed
+144 -52

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
"@medusajs/workflow-engine-inmemory": patch
"@medusajs/workflow-engine-redis": patch
---
chore(): only execute race execution checks for async workflows
@@ -47,11 +47,11 @@ import { createScheduled } from "../__fixtures__/workflow_scheduled"
jest.setTimeout(60000) jest.setTimeout(60000)
const failTrap = (done) => { const failTrap = (done, name) => {
setTimeoutSync(() => { setTimeoutSync(() => {
// REF:https://stackoverflow.com/questions/78028715/jest-async-test-with-event-emitter-isnt-ending // REF:https://stackoverflow.com/questions/78028715/jest-async-test-with-event-emitter-isnt-ending
console.warn( console.warn(
`Jest is breaking the event emit with its debouncer. This allows to continue the test by managing the timeout of the test manually.` `Jest is breaking the event emit with its debouncer. This allows to continue the test by managing the timeout of the test manually. ${name}`
) )
done() done()
}, 5000) }, 5000)
@@ -200,7 +200,10 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
}) })
}) })
failTrap(done) failTrap(
done,
"should cancel an ongoing execution with async unfinished yet step"
)
}) })
it("should cancel a complete execution with a sync workflow running as async", async () => { it("should cancel a complete execution with a sync workflow running as async", async () => {
@@ -537,7 +540,10 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
}) })
}) })
failTrap(done) failTrap(
done,
"should subscribe to a async workflow and receive the response when it finishes"
)
}) })
describe("Testing basic workflow", function () { describe("Testing basic workflow", function () {
@@ -744,7 +750,10 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
}) })
expect(onFinish).toHaveBeenCalledTimes(0) expect(onFinish).toHaveBeenCalledTimes(0)
failTrap(done) failTrap(
done,
"should subscribe to a async workflow and receive the response when it finishes"
)
}) })
it("should cancel and revert a completed workflow", async () => { it("should cancel and revert a completed workflow", async () => {
@@ -817,7 +826,10 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
throwOnError: true, throwOnError: true,
}) })
failTrap(done) failTrap(
done,
"should not run conditional steps if condition is false"
)
}) })
it("should not run conditional steps if condition is false", (done) => { it("should not run conditional steps if condition is false", (done) => {
@@ -839,7 +851,10 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
throwOnError: true, throwOnError: true,
}) })
failTrap(done) failTrap(
done,
"should not run conditional steps if condition is false"
)
}) })
}) })
@@ -988,7 +1003,10 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
}, },
}) })
failTrap(done) failTrap(
done,
"should display error when multple async steps are running in parallel"
)
}) })
}) })
@@ -14,11 +14,11 @@ import "../__fixtures__"
jest.setTimeout(300000) jest.setTimeout(300000)
const failTrap = (done) => { const failTrap = (done, name) => {
setTimeoutSync(() => { setTimeoutSync(() => {
// REF:https://stackoverflow.com/questions/78028715/jest-async-test-with-event-emitter-isnt-ending // REF:https://stackoverflow.com/questions/78028715/jest-async-test-with-event-emitter-isnt-ending
console.warn( console.warn(
"Jest is breaking the event emit with its debouncer. This allows to continue the test by managing the timeout of the test manually." `Jest is breaking the event emit with its debouncer. This allows to continue the test by managing the timeout of the test manually. ${name}`
) )
done() done()
}, 5000) }, 5000)
@@ -99,7 +99,10 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
}) })
.catch((e) => e) .catch((e) => e)
failTrap(done) failTrap(
done,
"should prevent race continuation of the workflow during retryIntervalAwaiting in background execution"
)
}) })
it("should prevent race continuation of the workflow compensation during retryIntervalAwaiting in background execution", (done) => { it("should prevent race continuation of the workflow compensation during retryIntervalAwaiting in background execution", (done) => {
@@ -190,7 +193,10 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
}) })
.catch((e) => e) .catch((e) => e)
failTrap(done) failTrap(
done,
"should prevent race continuation of the workflow compensation during retryIntervalAwaiting in background execution"
)
}) })
}) })
}, },
@@ -285,11 +285,13 @@ export class InMemoryDistributedTransactionStorage
const { retentionTime } = options ?? {} const { retentionTime } = options ?? {}
await this.#preventRaceConditionExecutionIfNecessary({ if (data.flow.hasAsyncSteps) {
data, await this.#preventRaceConditionExecutionIfNecessary({
key, data,
options, key,
}) options,
})
}
// Only store retention time if it's provided // Only store retention time if it's provided
if (retentionTime) { if (retentionTime) {
@@ -15,11 +15,11 @@ import "../__fixtures__"
jest.setTimeout(300000) jest.setTimeout(300000)
const failTrap = (done) => { const failTrap = (done, name) => {
setTimeoutSync(() => { setTimeoutSync(() => {
// REF:https://stackoverflow.com/questions/78028715/jest-async-test-with-event-emitter-isnt-ending // REF:https://stackoverflow.com/questions/78028715/jest-async-test-with-event-emitter-isnt-ending
console.warn( console.warn(
"Jest is breaking the event emit with its debouncer. This allows to continue the test by managing the timeout of the test manually." `Jest is breaking the event emit with its debouncer. This allows to continue the test by managing the timeout of the test manually. ${name}`
) )
done() done()
}, 5000) }, 5000)
@@ -118,7 +118,10 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
expect(result).toBe("result from step 0") expect(result).toBe("result from step 0")
}) })
failTrap(done) failTrap(
done,
"should prevent race continuation of the workflow during retryIntervalAwaiting in background execution"
)
}) })
it("should prevent race continuation of the workflow compensation during retryIntervalAwaiting in background execution", (done) => { it("should prevent race continuation of the workflow compensation during retryIntervalAwaiting in background execution", (done) => {
@@ -214,7 +217,10 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
expect(result).toBe("result from step 0") expect(result).toBe("result from step 0")
}) })
failTrap(done) failTrap(
done,
"should prevent race continuation of the workflow compensation during retryIntervalAwaiting in background execution"
)
}) })
}) })
}, },
@@ -64,6 +64,7 @@ type IdempotencyKeyParts = {
type NotifyOptions = { type NotifyOptions = {
eventType: keyof DistributedTransactionEvents eventType: keyof DistributedTransactionEvents
isFlowAsync: boolean
workflowId: string workflowId: string
transactionId?: string transactionId?: string
step?: TransactionStep step?: TransactionStep
@@ -315,12 +316,6 @@ export class WorkflowOrchestratorService {
throw new Error("Transaction ID is required") throw new Error("Transaction ID is required")
} }
const events: FlowRunOptions["events"] = this.buildWorkflowEvents({
customEventHandlers: eventHandlers,
workflowId,
transactionId: transactionId,
})
const exportedWorkflow = MedusaWorkflow.getWorkflow(workflowId) const exportedWorkflow = MedusaWorkflow.getWorkflow(workflowId)
if (!exportedWorkflow) { if (!exportedWorkflow) {
throw new Error(`Workflow with id "${workflowId}" not found.`) throw new Error(`Workflow with id "${workflowId}" not found.`)
@@ -344,6 +339,12 @@ export class WorkflowOrchestratorService {
throw new Error("Transaction not found") throw new Error("Transaction not found")
} }
const events: FlowRunOptions["events"] = this.buildWorkflowEvents({
customEventHandlers: eventHandlers,
workflowId,
transactionId: transactionId,
})
const ret = await exportedWorkflow.cancel({ const ret = await exportedWorkflow.cancel({
transaction, transaction,
throwOnError: false, throwOnError: false,
@@ -373,6 +374,7 @@ export class WorkflowOrchestratorService {
const { result, errors } = ret const { result, errors } = ret
this.notify({ this.notify({
isFlowAsync: ret.transaction.getFlow().hasAsyncSteps,
eventType: "onFinish", eventType: "onFinish",
workflowId, workflowId,
transactionId: transaction.transactionId, transactionId: transaction.transactionId,
@@ -471,6 +473,7 @@ export class WorkflowOrchestratorService {
const { result, errors } = ret const { result, errors } = ret
this.notify({ this.notify({
isFlowAsync: ret.transaction.getFlow().hasAsyncSteps,
eventType: "onFinish", eventType: "onFinish",
workflowId, workflowId,
transactionId, transactionId,
@@ -544,6 +547,7 @@ export class WorkflowOrchestratorService {
const { result, errors } = ret const { result, errors } = ret
this.notify({ this.notify({
isFlowAsync: ret.transaction.getFlow().hasAsyncSteps,
eventType: "onFinish", eventType: "onFinish",
workflowId, workflowId,
transactionId, transactionId,
@@ -669,16 +673,8 @@ export class WorkflowOrchestratorService {
return return
} }
if (publish) {
const channel = this.getChannelName(options.workflowId)
const message = JSON.stringify({
instanceId: this.instanceId,
data: options,
})
await this.redisPublisher.publish(channel, message)
}
const { const {
isFlowAsync,
eventType, eventType,
workflowId, workflowId,
transactionId, transactionId,
@@ -689,6 +685,15 @@ export class WorkflowOrchestratorService {
state, state,
} = options } = options
if (publish && isFlowAsync) {
const channel = this.getChannelName(options.workflowId)
const message = JSON.stringify({
instanceId: this.instanceId,
data: options,
})
await this.redisPublisher.publish(channel, message)
}
const subscribers: TransactionSubscribers = const subscribers: TransactionSubscribers =
this.subscribers.get(workflowId) ?? new Map() this.subscribers.get(workflowId) ?? new Map()
@@ -698,6 +703,7 @@ export class WorkflowOrchestratorService {
eventType, eventType,
workflowId, workflowId,
transactionId, transactionId,
isFlowAsync,
step, step,
response, response,
result, result,
@@ -743,6 +749,7 @@ export class WorkflowOrchestratorService {
transactionId, transactionId,
}): DistributedTransactionEvents { }): DistributedTransactionEvents {
const notify = async ({ const notify = async ({
isFlowAsync,
eventType, eventType,
step, step,
result, result,
@@ -750,6 +757,7 @@ export class WorkflowOrchestratorService {
errors, errors,
state, state,
}: { }: {
isFlowAsync: boolean
eventType: keyof DistributedTransactionEvents eventType: keyof DistributedTransactionEvents
step?: TransactionStep step?: TransactionStep
response?: unknown response?: unknown
@@ -758,6 +766,7 @@ export class WorkflowOrchestratorService {
state?: TransactionState state?: TransactionState
}) => { }) => {
await this.notify({ await this.notify({
isFlowAsync,
workflowId, workflowId,
transactionId, transactionId,
eventType, eventType,
@@ -772,31 +781,50 @@ export class WorkflowOrchestratorService {
return { return {
onTimeout: async ({ transaction }) => { onTimeout: async ({ transaction }) => {
customEventHandlers?.onTimeout?.({ transaction }) customEventHandlers?.onTimeout?.({ transaction })
await notify({ eventType: "onTimeout" }) await notify({
eventType: "onTimeout",
isFlowAsync: transaction.getFlow().hasAsyncSteps,
})
}, },
onBegin: async ({ transaction }) => { onBegin: async ({ transaction }) => {
customEventHandlers?.onBegin?.({ transaction }) customEventHandlers?.onBegin?.({ transaction })
await notify({ eventType: "onBegin" }) await notify({
eventType: "onBegin",
isFlowAsync: transaction.getFlow().hasAsyncSteps,
})
}, },
onResume: async ({ transaction }) => { onResume: async ({ transaction }) => {
customEventHandlers?.onResume?.({ transaction }) customEventHandlers?.onResume?.({ transaction })
await notify({ eventType: "onResume" }) await notify({
eventType: "onResume",
isFlowAsync: transaction.getFlow().hasAsyncSteps,
})
}, },
onCompensateBegin: async ({ transaction }) => { onCompensateBegin: async ({ transaction }) => {
customEventHandlers?.onCompensateBegin?.({ transaction }) customEventHandlers?.onCompensateBegin?.({ transaction })
await notify({ eventType: "onCompensateBegin" }) await notify({
eventType: "onCompensateBegin",
isFlowAsync: transaction.getFlow().hasAsyncSteps,
})
}, },
onFinish: async ({ transaction, result, errors }) => { onFinish: async ({ transaction, result, errors }) => {
customEventHandlers?.onFinish?.({ transaction, result, errors }) customEventHandlers?.onFinish?.({ transaction, result, errors })
await notify({ eventType: "onFinish" }) await notify({
eventType: "onFinish",
isFlowAsync: transaction.getFlow().hasAsyncSteps,
})
}, },
onStepBegin: async ({ step, transaction }) => { onStepBegin: async ({ step, transaction }) => {
customEventHandlers?.onStepBegin?.({ step, transaction }) customEventHandlers?.onStepBegin?.({ step, transaction })
this.activeStepsCount++ this.activeStepsCount++
await notify({ eventType: "onStepBegin", step }) await notify({
eventType: "onStepBegin",
step,
isFlowAsync: transaction.getFlow().hasAsyncSteps,
})
}, },
onStepSuccess: async ({ step, transaction }) => { onStepSuccess: async ({ step, transaction }) => {
const stepName = step.definition.action! const stepName = step.definition.action!
@@ -805,7 +833,12 @@ export class WorkflowOrchestratorService {
transaction transaction
) )
customEventHandlers?.onStepSuccess?.({ step, transaction, response }) customEventHandlers?.onStepSuccess?.({ step, transaction, response })
await notify({ eventType: "onStepSuccess", step, response }) await notify({
eventType: "onStepSuccess",
step,
response,
isFlowAsync: transaction.getFlow().hasAsyncSteps,
})
this.activeStepsCount-- this.activeStepsCount--
}, },
@@ -816,14 +849,23 @@ export class WorkflowOrchestratorService {
.filter((err) => err.action === stepName) .filter((err) => err.action === stepName)
customEventHandlers?.onStepFailure?.({ step, transaction, errors }) customEventHandlers?.onStepFailure?.({ step, transaction, errors })
await notify({ eventType: "onStepFailure", step, errors }) await notify({
eventType: "onStepFailure",
step,
errors,
isFlowAsync: transaction.getFlow().hasAsyncSteps,
})
this.activeStepsCount-- this.activeStepsCount--
}, },
onStepAwaiting: async ({ step, transaction }) => { onStepAwaiting: async ({ step, transaction }) => {
customEventHandlers?.onStepAwaiting?.({ step, transaction }) customEventHandlers?.onStepAwaiting?.({ step, transaction })
await notify({ eventType: "onStepAwaiting", step }) await notify({
eventType: "onStepAwaiting",
step,
isFlowAsync: transaction.getFlow().hasAsyncSteps,
})
this.activeStepsCount-- this.activeStepsCount--
}, },
@@ -837,7 +879,12 @@ export class WorkflowOrchestratorService {
response, response,
}) })
await notify({ eventType: "onCompensateStepSuccess", step, response }) await notify({
eventType: "onCompensateStepSuccess",
step,
response,
isFlowAsync: transaction.getFlow().hasAsyncSteps,
})
}, },
onCompensateStepFailure: async ({ step, transaction }) => { onCompensateStepFailure: async ({ step, transaction }) => {
const stepName = step.definition.action! const stepName = step.definition.action!
@@ -847,7 +894,12 @@ export class WorkflowOrchestratorService {
customEventHandlers?.onStepFailure?.({ step, transaction, errors }) customEventHandlers?.onStepFailure?.({ step, transaction, errors })
await notify({ eventType: "onCompensateStepFailure", step, errors }) await notify({
eventType: "onCompensateStepFailure",
step,
errors,
isFlowAsync: transaction.getFlow().hasAsyncSteps,
})
}, },
} }
} }
@@ -413,11 +413,13 @@ export class RedisDistributedTransactionStorage
const { retentionTime } = options ?? {} const { retentionTime } = options ?? {}
await this.#preventRaceConditionExecutionIfNecessary({ if (data.flow.hasAsyncSteps) {
data, await this.#preventRaceConditionExecutionIfNecessary({
key, data,
options, key,
}) options,
})
}
if (hasFinished && retentionTime) { if (hasFinished && retentionTime) {
Object.assign(data, { Object.assign(data, {