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:
Adrien de Peretti
2025-10-20 15:29:19 +02:00
committed by GitHub
parent d97a60d3c1
commit 516f5a3896
31 changed files with 2712 additions and 1406 deletions

View File

@@ -8,6 +8,7 @@ import {
normalizeImportPathWithSource,
toMikroOrmEntities,
} from "@medusajs/framework/utils"
import { logger } from "@medusajs/framework/logger"
import * as fs from "fs"
import { getDatabaseURL, getMikroOrmWrapper, TestDatabase } from "./database"
import { initModules, InitModulesOptions } from "./init-modules"
@@ -23,6 +24,24 @@ export interface SuiteOptions<TService = unknown> {
}
}
interface ModuleTestRunnerConfig<TService = any> {
moduleName: string
moduleModels?: any[]
moduleOptions?: Record<string, any>
moduleDependencies?: string[]
joinerConfig?: any[]
schema?: string
dbName?: string
injectedDependencies?: Record<string, any>
resolve?: string
debug?: boolean
cwd?: string
hooks?: {
beforeModuleInit?: () => Promise<void>
afterModuleInit?: (medusaApp: any, service: TService) => Promise<void>
}
}
function createMikroOrmWrapper(options: {
moduleModels?: (Function | DmlEntity<any, any>)[]
resolve?: string
@@ -64,6 +83,220 @@ function createMikroOrmWrapper(options: {
return { MikroOrmWrapper, models: moduleModels }
}
class ModuleTestRunner<TService = any> {
private moduleName: string
private schema: string
private dbName: string
private dbConfig: {
clientUrl: string
schema: string
debug: boolean
}
private debug: boolean
private resolve?: string
private cwd?: string
private moduleOptions: Record<string, any>
private moduleDependencies?: string[]
private joinerConfig: any[]
private injectedDependencies: Record<string, any>
private hooks: ModuleTestRunnerConfig<TService>["hooks"] = {}
private connection: any = null
private MikroOrmWrapper!: TestDatabase
private moduleModels: (Function | DmlEntity<any, any>)[] = []
private modulesConfig: any = {}
private moduleOptionsConfig!: InitModulesOptions
private shutdown: () => Promise<void> = async () => void 0
private moduleService: any = null
private medusaApp: any = {}
constructor(config: ModuleTestRunnerConfig<TService>) {
const tempName = parseInt(process.env.JEST_WORKER_ID || "1")
this.moduleName = config.moduleName
this.dbName =
config.dbName ??
`medusa-${config.moduleName.toLowerCase()}-integration-${tempName}`
this.schema = config.schema ?? "public"
this.debug = config.debug ?? false
this.resolve = config.resolve
this.cwd = config.cwd
this.moduleOptions = config.moduleOptions ?? {}
this.moduleDependencies = config.moduleDependencies
this.joinerConfig = config.joinerConfig ?? []
this.injectedDependencies = config.injectedDependencies ?? {}
this.hooks = config.hooks ?? {}
this.dbConfig = {
clientUrl: getDatabaseURL(this.dbName),
schema: this.schema,
debug: this.debug,
}
this.setupProcessHandlers()
this.initializeConfig(config.moduleModels)
}
private setupProcessHandlers(): void {
process.on("SIGTERM", async () => {
await this.cleanup()
process.exit(0)
})
process.on("SIGINT", async () => {
await this.cleanup()
process.exit(0)
})
}
private initializeConfig(moduleModels?: any[]): void {
const moduleSdkImports = require("@medusajs/framework/modules-sdk")
// Use a unique connection for all the entire suite
this.connection = ModulesSdkUtils.createPgConnection(this.dbConfig)
const { MikroOrmWrapper, models } = createMikroOrmWrapper({
moduleModels,
resolve: this.resolve,
dbConfig: this.dbConfig,
cwd: this.cwd,
})
this.MikroOrmWrapper = MikroOrmWrapper
this.moduleModels = models
this.modulesConfig = {
[this.moduleName]: {
definition: moduleSdkImports.ModulesDefinition[this.moduleName],
resolve: this.resolve,
dependencies: this.moduleDependencies,
options: {
database: this.dbConfig,
...this.moduleOptions,
[isSharedConnectionSymbol]: true,
},
},
}
this.moduleOptionsConfig = {
injectedDependencies: {
[ContainerRegistrationKeys.PG_CONNECTION]: this.connection,
[Modules.EVENT_BUS]: new MockEventBusService(),
[ContainerRegistrationKeys.LOGGER]: console,
...this.injectedDependencies,
},
modulesConfig: this.modulesConfig,
databaseConfig: this.dbConfig,
joinerConfig: this.joinerConfig,
preventConnectionDestroyWarning: true,
cwd: this.cwd,
}
}
private createMedusaAppProxy(): any {
return new Proxy(
{},
{
get: (target, prop) => {
return this.medusaApp?.[prop]
},
}
)
}
private createServiceProxy(): any {
return new Proxy(
{},
{
get: (target, prop) => {
return this.moduleService?.[prop]
},
}
)
}
public async beforeAll(): Promise<void> {
try {
this.setupProcessHandlers()
process.env.LOG_LEVEL = "error"
} catch (error) {
await this.cleanup()
throw error
}
}
public async beforeEach(): Promise<void> {
try {
if (this.moduleModels.length) {
await this.MikroOrmWrapper.setupDatabase()
}
if (this.hooks?.beforeModuleInit) {
await this.hooks.beforeModuleInit()
}
const output = await initModules(this.moduleOptionsConfig)
this.shutdown = output.shutdown
this.medusaApp = output.medusaApp
this.moduleService = output.medusaApp.modules[this.moduleName]
if (this.hooks?.afterModuleInit) {
await this.hooks.afterModuleInit(this.medusaApp, this.moduleService)
}
} catch (error) {
logger.error("Error in beforeEach:", error?.message)
await this.cleanup()
throw error
}
}
public async afterEach(): Promise<void> {
try {
if (this.moduleModels.length) {
await this.MikroOrmWrapper.clearDatabase()
}
await this.shutdown()
this.moduleService = {}
this.medusaApp = {}
} catch (error) {
logger.error("Error in afterEach:", error?.message)
throw error
}
}
public async cleanup(): Promise<void> {
try {
process.removeAllListeners("SIGTERM")
process.removeAllListeners("SIGINT")
await (this.connection as any)?.context?.destroy()
await (this.connection as any)?.destroy()
this.moduleService = null
this.medusaApp = null
this.connection = null
if (global.gc) {
global.gc()
}
} catch (error) {
logger.error("Error during cleanup:", error?.message)
}
}
public getOptions(): SuiteOptions<TService> {
return {
MikroOrmWrapper: this.MikroOrmWrapper,
medusaApp: this.createMedusaAppProxy(),
service: this.createServiceProxy(),
dbConfig: {
schema: this.schema,
clientUrl: this.dbConfig.clientUrl,
},
}
}
}
export function moduleIntegrationTestRunner<TService = any>({
moduleName,
moduleModels,
@@ -76,6 +309,7 @@ export function moduleIntegrationTestRunner<TService = any>({
resolve,
injectedDependencies = {},
cwd,
hooks,
}: {
moduleName: string
moduleModels?: any[]
@@ -88,115 +322,68 @@ export function moduleIntegrationTestRunner<TService = any>({
resolve?: string
debug?: boolean
cwd?: string
hooks?: ModuleTestRunnerConfig<TService>["hooks"]
testSuite: (options: SuiteOptions<TService>) => void
}) {
const moduleSdkImports = require("@medusajs/framework/modules-sdk")
process.env.LOG_LEVEL = "error"
const tempName = parseInt(process.env.JEST_WORKER_ID || "1")
const dbName = `medusa-${moduleName.toLowerCase()}-integration-${tempName}`
const dbConfig = {
clientUrl: getDatabaseURL(dbName),
const runner = new ModuleTestRunner<TService>({
moduleName,
moduleModels,
moduleOptions,
moduleDependencies,
joinerConfig,
schema,
debug,
}
// Use a unique connection for all the entire suite
const connection = ModulesSdkUtils.createPgConnection(dbConfig)
const { MikroOrmWrapper, models } = createMikroOrmWrapper({
moduleModels,
resolve,
dbConfig,
injectedDependencies,
cwd,
hooks,
})
moduleModels = models
const modulesConfig_ = {
[moduleName]: {
definition: moduleSdkImports.ModulesDefinition[moduleName],
resolve,
dependencies: moduleDependencies,
options: {
database: dbConfig,
...moduleOptions,
[isSharedConnectionSymbol]: true,
},
},
}
const moduleOptions_: InitModulesOptions = {
injectedDependencies: {
[ContainerRegistrationKeys.PG_CONNECTION]: connection,
[Modules.EVENT_BUS]: new MockEventBusService(),
[ContainerRegistrationKeys.LOGGER]: console,
...injectedDependencies,
},
modulesConfig: modulesConfig_,
databaseConfig: dbConfig,
joinerConfig,
preventConnectionDestroyWarning: true,
cwd,
}
let shutdown: () => Promise<void>
let moduleService
let medusaApp = {}
const options = {
MikroOrmWrapper,
medusaApp: new Proxy(
{},
{
get: (target, prop) => {
return medusaApp[prop]
},
}
),
service: new Proxy(
{},
{
get: (target, prop) => {
return moduleService[prop]
},
}
),
dbConfig: {
schema,
clientUrl: dbConfig.clientUrl,
},
} as SuiteOptions<TService>
const beforeEach_ = async () => {
if (moduleModels.length) {
await MikroOrmWrapper.setupDatabase()
}
const output = await initModules(moduleOptions_)
shutdown = output.shutdown
medusaApp = output.medusaApp
moduleService = output.medusaApp.modules[moduleName]
}
const afterEach_ = async () => {
if (moduleModels.length) {
await MikroOrmWrapper.clearDatabase()
}
await shutdown()
moduleService = {}
medusaApp = {}
}
return describe("", () => {
beforeEach(beforeEach_)
afterEach(afterEach_)
afterAll(async () => {
await (connection as any).context?.destroy()
await (connection as any).destroy()
let testOptions: SuiteOptions<TService>
beforeAll(async () => {
await runner.beforeAll()
testOptions = runner.getOptions()
})
testSuite(options)
beforeEach(async () => {
await runner.beforeEach()
})
afterEach(async () => {
await runner.afterEach()
})
afterAll(async () => {
// Run main cleanup
await runner.cleanup()
// Clean references to the test options
for (const key in testOptions) {
if (typeof testOptions[key] === "function") {
testOptions[key] = null
} else if (
typeof testOptions[key] === "object" &&
testOptions[key] !== null
) {
Object.keys(testOptions[key]).forEach((k) => {
testOptions[key][k] = null
})
testOptions[key] = null
}
}
// Encourage garbage collection
// @ts-ignore
testOptions = null
if (global.gc) {
global.gc()
}
})
// Run test suite with options
testSuite(runner.getOptions())
})
}