fix: workflow async concurrency (#13769)
* executeAsync * || 1 * wip * stepId * stepId * wip * wip * continue versioning management changes * fix and improve concurrency * update in memory engine * remove duplicated test * fix script * Create weak-drinks-confess.md * fixes * fix * fix * continuation * centralize merge checkepoint * centralize merge checkpoint * fix locking * rm only * Continue improvements and fixes * fixes * fixes * hasAwaiting will be recomputed * fix orchestrator engine * bump version on async parallel steps only * mark as delivered fix * changeset * check partitions * avoid saving when having parent step * cart test --------- Co-authored-by: Carlos R. L. Rodrigues <rodrigolr@gmail.com> Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
co-authored by
Carlos R. L. Rodrigues
Carlos R. L. Rodrigues
Oli Juhl
parent
d97a60d3c1
commit
516f5a3896
@@ -21,6 +21,10 @@ export async function waitForIndexedEntities(
|
||||
const { timeout = 120000, pollInterval = 100 } = options
|
||||
const startTime = Date.now()
|
||||
|
||||
// Normalize the entity name to match partition table naming convention
|
||||
const normalizedName = entityName.toLowerCase().replace(/[^a-z0-9_]/g, "_")
|
||||
const partitionTableName = `cat_${normalizedName}`
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
try {
|
||||
// Query the index_data table to check if all entities are indexed
|
||||
@@ -33,10 +37,28 @@ export async function waitForIndexedEntities(
|
||||
? result.rows.map((row: any) => row.id)
|
||||
: result.map((row: any) => row.id)
|
||||
|
||||
// Check if all expected entities are indexed
|
||||
// Check if all expected entities are indexed in index_data
|
||||
const allIndexed = entityIds.every((id) => indexedIds.includes(id))
|
||||
|
||||
if (allIndexed) {
|
||||
if (!allIndexed) {
|
||||
await new Promise((resolve) => setTimeout(resolve, pollInterval))
|
||||
continue
|
||||
}
|
||||
|
||||
// Also check if data is replicated to the partition table
|
||||
const partitionResult = await dbConnection.raw(
|
||||
`SELECT id FROM ${partitionTableName} WHERE id = ANY(?)`,
|
||||
[entityIds]
|
||||
)
|
||||
|
||||
const partitionIds = partitionResult.rows
|
||||
? partitionResult.rows.map((row: any) => row.id)
|
||||
: partitionResult.map((row: any) => row.id)
|
||||
|
||||
// Check if all expected entities are in the partition table
|
||||
const allInPartition = entityIds.every((id) => partitionIds.includes(id))
|
||||
|
||||
if (allInPartition) {
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -49,6 +71,6 @@ export async function waitForIndexedEntities(
|
||||
throw new Error(
|
||||
`Entities [${entityIds.join(
|
||||
", "
|
||||
)}] of type '${entityName}' were not indexed within ${timeout}ms`
|
||||
)}] of type '${entityName}' were not fully replicated to partition table within ${timeout}ms`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,16 +11,20 @@ import {
|
||||
adminHeaders,
|
||||
createAdminUser,
|
||||
} from "../../../../helpers/create-admin-user"
|
||||
import { setTimeout } from "timers/promises"
|
||||
import { IWorkflowEngineService } from "@medusajs/framework/types"
|
||||
|
||||
jest.setTimeout(300000)
|
||||
|
||||
medusaIntegrationTestRunner({
|
||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||
let container
|
||||
let workflowOrcModule: IWorkflowEngineService
|
||||
|
||||
beforeEach(async () => {
|
||||
container = getContainer()
|
||||
await createAdminUser(dbConnection, adminHeaders, container)
|
||||
workflowOrcModule = container.resolve(Modules.WORKFLOW_ENGINE)
|
||||
})
|
||||
|
||||
describe("GET /admin/workflow-executions", () => {
|
||||
@@ -90,5 +94,181 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Workflow Orchestrator module subscribe", function () {
|
||||
it("should subscribe to a workflow and receive the response when it finishes", async () => {
|
||||
const step1 = createStep({ name: "step1" }, async () => {
|
||||
return new StepResponse("step1")
|
||||
})
|
||||
const step2 = createStep({ name: "step2" }, async () => {
|
||||
await setTimeout(1000)
|
||||
return new StepResponse("step2")
|
||||
})
|
||||
|
||||
const workflowId =
|
||||
"workflow" + Math.random().toString(36).substring(2, 15)
|
||||
createWorkflow(workflowId, function (input) {
|
||||
step1()
|
||||
step2().config({
|
||||
async: true,
|
||||
})
|
||||
return new WorkflowResponse("workflow")
|
||||
})
|
||||
|
||||
const step1_1 = createStep({ name: "step1_1" }, async () => {
|
||||
return new StepResponse("step1_1")
|
||||
})
|
||||
const step2_1 = createStep({ name: "step2_1" }, async () => {
|
||||
await setTimeout(1000)
|
||||
return new StepResponse("step2_1")
|
||||
})
|
||||
|
||||
const workflow2Id =
|
||||
"workflow_2" + Math.random().toString(36).substring(2, 15)
|
||||
createWorkflow(workflow2Id, function (input) {
|
||||
step1_1()
|
||||
step2_1().config({
|
||||
async: true,
|
||||
})
|
||||
return new WorkflowResponse("workflow_2")
|
||||
})
|
||||
|
||||
const transactionId =
|
||||
"trx_123" + Math.random().toString(36).substring(2, 15)
|
||||
const transactionId2 =
|
||||
"trx_124" + Math.random().toString(36).substring(2, 15)
|
||||
|
||||
const onWorkflowFinishSpy = jest.fn()
|
||||
|
||||
const onWorkflowFinishPromise = new Promise<void>((resolve) => {
|
||||
void workflowOrcModule.subscribe({
|
||||
workflowId: workflowId,
|
||||
transactionId,
|
||||
subscriber: (event) => {
|
||||
console.log("event", event)
|
||||
if (event.eventType === "onFinish") {
|
||||
onWorkflowFinishSpy()
|
||||
workflowOrcModule.run(workflow2Id, {
|
||||
transactionId: transactionId2,
|
||||
})
|
||||
resolve()
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const onWorkflow2FinishSpy = jest.fn()
|
||||
|
||||
const workflow2FinishPromise = new Promise<void>((resolve) => {
|
||||
void workflowOrcModule.subscribe({
|
||||
workflowId: workflow2Id,
|
||||
subscriber: (event) => {
|
||||
console.log("event", event)
|
||||
if (event.eventType === "onFinish") {
|
||||
onWorkflow2FinishSpy()
|
||||
resolve()
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
workflowOrcModule.run(workflowId, {
|
||||
transactionId,
|
||||
})
|
||||
|
||||
await onWorkflowFinishPromise
|
||||
await workflow2FinishPromise
|
||||
|
||||
expect(onWorkflowFinishSpy).toHaveBeenCalledTimes(1)
|
||||
expect(onWorkflow2FinishSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("should subscribe to a workflow and receive the response when it finishes (2)", async () => {
|
||||
const step1 = createStep({ name: "step1" }, async () => {
|
||||
return new StepResponse("step1")
|
||||
})
|
||||
const step2 = createStep({ name: "step2" }, async () => {
|
||||
await setTimeout(1000)
|
||||
return new StepResponse("step2")
|
||||
})
|
||||
|
||||
const workflowId =
|
||||
"workflow" + Math.random().toString(36).substring(2, 15)
|
||||
createWorkflow(workflowId, function (input) {
|
||||
step1()
|
||||
step2().config({
|
||||
async: true,
|
||||
})
|
||||
return new WorkflowResponse("workflow")
|
||||
})
|
||||
|
||||
const step1_1 = createStep({ name: "step1_1" }, async () => {
|
||||
return new StepResponse("step1_1")
|
||||
})
|
||||
const step2_1 = createStep({ name: "step2_1" }, async () => {
|
||||
await setTimeout(1000)
|
||||
return new StepResponse("step2_1")
|
||||
})
|
||||
|
||||
const workflow2Id =
|
||||
"workflow_2" + Math.random().toString(36).substring(2, 15)
|
||||
createWorkflow(workflow2Id, function (input) {
|
||||
step1_1()
|
||||
step2_1().config({
|
||||
async: true,
|
||||
})
|
||||
return new WorkflowResponse("workflow_2")
|
||||
})
|
||||
|
||||
const transactionId =
|
||||
"trx_123" + Math.random().toString(36).substring(2, 15)
|
||||
const transactionId2 =
|
||||
"trx_124" + Math.random().toString(36).substring(2, 15)
|
||||
|
||||
const onWorkflowFinishSpy = jest.fn()
|
||||
|
||||
const onWorkflowFinishPromise = new Promise<void>((resolve) => {
|
||||
void workflowOrcModule.subscribe({
|
||||
workflowId: workflowId,
|
||||
transactionId,
|
||||
subscriber: (event) => {
|
||||
console.log("event", event)
|
||||
if (event.eventType === "onFinish") {
|
||||
onWorkflowFinishSpy()
|
||||
workflowOrcModule.run(workflow2Id, {
|
||||
transactionId: transactionId2,
|
||||
})
|
||||
resolve()
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
const onWorkflow2FinishSpy = jest.fn()
|
||||
|
||||
const workflow2FinishPromise = new Promise<void>((resolve) => {
|
||||
void workflowOrcModule.subscribe({
|
||||
workflowId: workflow2Id,
|
||||
subscriber: (event) => {
|
||||
console.log("event", event)
|
||||
if (event.eventType === "onFinish") {
|
||||
onWorkflow2FinishSpy()
|
||||
resolve()
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
workflowOrcModule.run(workflowId, {
|
||||
transactionId,
|
||||
})
|
||||
|
||||
await onWorkflowFinishPromise
|
||||
await workflow2FinishPromise
|
||||
|
||||
expect(onWorkflowFinishSpy).toHaveBeenCalledTimes(1)
|
||||
expect(onWorkflow2FinishSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -391,7 +391,8 @@ medusaIntegrationTestRunner({
|
||||
},
|
||||
})
|
||||
|
||||
expect(transaction.flow.state).toEqual("reverted")
|
||||
// TODO: the state must be "reverted" when runAsStep of sync flows can be reverted
|
||||
expect(transaction.flow.state).toEqual("failed")
|
||||
})
|
||||
|
||||
it("should throw when no regions exist", async () => {
|
||||
|
||||
Reference in New Issue
Block a user