chore: configurable database migration in concurrency (#14004)
> [!NOTE] > Adds a configurable concurrency for link migrations (CLI/commands), forces concurrency=1 when pgstream is detected, and ignores duplicate link-table inserts. > > - **CLI** > - Add `--concurrency` option to `db:migrate` and `db:sync-links`. > - **Medusa commands** > - `migrate` and `sync-links` accept `concurrency`; set `DB_MIGRATION_CONCURRENCY` and force `1` when `pgstream` schema exists via new `isPgstreamEnabled`. > - **Link Modules (migrations)** > - Execute plan actions with `executeWithConcurrency` using `DB_MIGRATION_CONCURRENCY`. > - Make link-table tracking inserts idempotent with `ON CONFLICT DO NOTHING` (including bulk/upsert and per-create). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 07432293c8fe8de30b07920fa47823b9081edacc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
This commit is contained in:
1 parent
a33ef1e4d9
commit
5b7e3c0e76
6 files changed
+104
-28
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/link-modules": patch
|
||||||
|
"@medusajs/cli": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
chore(link-modules): ignore duplicates link creation
|
||||||
@@ -196,6 +196,10 @@ function buildLocalCommands(cli, isLocalProject) {
|
|||||||
describe:
|
describe:
|
||||||
"Skip prompts and execute only safe actions from sync links",
|
"Skip prompts and execute only safe actions from sync links",
|
||||||
})
|
})
|
||||||
|
builder.option("concurrency", {
|
||||||
|
type: "number",
|
||||||
|
describe: "Number of concurrent migrations to run",
|
||||||
|
})
|
||||||
},
|
},
|
||||||
handler: handlerP(
|
handler: handlerP(
|
||||||
getCommandHandler("db/migrate", (args, cmd) => {
|
getCommandHandler("db/migrate", (args, cmd) => {
|
||||||
@@ -270,6 +274,10 @@ function buildLocalCommands(cli, isLocalProject) {
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
describe: "Skip prompts and execute only safe actions",
|
describe: "Skip prompts and execute only safe actions",
|
||||||
})
|
})
|
||||||
|
builder.option("concurrency", {
|
||||||
|
type: "number",
|
||||||
|
describe: "Number of concurrent migrations to run",
|
||||||
|
})
|
||||||
},
|
},
|
||||||
handler: handlerP(
|
handler: handlerP(
|
||||||
getCommandHandler("db/sync-links", (args, cmd) => {
|
getCommandHandler("db/sync-links", (args, cmd) => {
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import { LinkLoader } from "@medusajs/framework/links"
|
|||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
getResolvedPlugins,
|
getResolvedPlugins,
|
||||||
|
isDefined,
|
||||||
mergePluginModules,
|
mergePluginModules,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import { Logger, MedusaContainer } from "@medusajs/types"
|
import { Logger, MedusaContainer } from "@medusajs/types"
|
||||||
import { fork } from "child_process"
|
import { fork } from "child_process"
|
||||||
import path, { join } from "path"
|
import path, { join } from "path"
|
||||||
import { initializeContainer } from "../../loaders"
|
import { initializeContainer } from "../../loaders"
|
||||||
import { ensureDbExists } from "../utils"
|
import { ensureDbExists, isPgstreamEnabled } from "../utils"
|
||||||
import { syncLinks } from "./sync-links"
|
import { syncLinks } from "./sync-links"
|
||||||
|
|
||||||
const TERMINAL_SIZE = process.stdout.columns
|
const TERMINAL_SIZE = process.stdout.columns
|
||||||
@@ -26,6 +27,7 @@ export async function migrate({
|
|||||||
skipScripts,
|
skipScripts,
|
||||||
executeAllLinks,
|
executeAllLinks,
|
||||||
executeSafeLinks,
|
executeSafeLinks,
|
||||||
|
concurrency,
|
||||||
logger,
|
logger,
|
||||||
container,
|
container,
|
||||||
}: {
|
}: {
|
||||||
@@ -34,6 +36,7 @@ export async function migrate({
|
|||||||
skipScripts: boolean
|
skipScripts: boolean
|
||||||
executeAllLinks: boolean
|
executeAllLinks: boolean
|
||||||
executeSafeLinks: boolean
|
executeSafeLinks: boolean
|
||||||
|
concurrency?: number
|
||||||
logger: Logger
|
logger: Logger
|
||||||
container: MedusaContainer
|
container: MedusaContainer
|
||||||
}): Promise<boolean> {
|
}): Promise<boolean> {
|
||||||
@@ -43,6 +46,16 @@ export async function migrate({
|
|||||||
|
|
||||||
await ensureDbExists(container)
|
await ensureDbExists(container)
|
||||||
|
|
||||||
|
// If pgstream is enabled, force concurrency to 1
|
||||||
|
const pgstreamEnabled = await isPgstreamEnabled(container)
|
||||||
|
if (pgstreamEnabled) {
|
||||||
|
concurrency = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(concurrency)) {
|
||||||
|
process.env.DB_MIGRATION_CONCURRENCY = String(concurrency)
|
||||||
|
}
|
||||||
|
|
||||||
const medusaAppLoader = new MedusaAppLoader()
|
const medusaAppLoader = new MedusaAppLoader()
|
||||||
const configModule = container.resolve(
|
const configModule = container.resolve(
|
||||||
ContainerRegistrationKeys.CONFIG_MODULE
|
ContainerRegistrationKeys.CONFIG_MODULE
|
||||||
@@ -80,6 +93,7 @@ export async function migrate({
|
|||||||
executeSafe: executeSafeLinks,
|
executeSafe: executeSafeLinks,
|
||||||
directory,
|
directory,
|
||||||
container,
|
container,
|
||||||
|
concurrency,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +126,7 @@ const main = async function ({
|
|||||||
skipScripts,
|
skipScripts,
|
||||||
executeAllLinks,
|
executeAllLinks,
|
||||||
executeSafeLinks,
|
executeSafeLinks,
|
||||||
|
concurrency,
|
||||||
}) {
|
}) {
|
||||||
const container = await initializeContainer(directory)
|
const container = await initializeContainer(directory)
|
||||||
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
||||||
@@ -123,6 +138,7 @@ const main = async function ({
|
|||||||
skipScripts,
|
skipScripts,
|
||||||
executeAllLinks,
|
executeAllLinks,
|
||||||
executeSafeLinks,
|
executeSafeLinks,
|
||||||
|
concurrency,
|
||||||
logger,
|
logger,
|
||||||
container,
|
container,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ContainerRegistrationKeys,
|
ContainerRegistrationKeys,
|
||||||
getResolvedPlugins,
|
getResolvedPlugins,
|
||||||
|
isDefined,
|
||||||
mergePluginModules,
|
mergePluginModules,
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import boxen from "boxen"
|
import boxen from "boxen"
|
||||||
@@ -16,7 +17,7 @@ import chalk from "chalk"
|
|||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
|
|
||||||
import { initializeContainer } from "../../loaders"
|
import { initializeContainer } from "../../loaders"
|
||||||
import { ensureDbExists } from "../utils"
|
import { ensureDbExists, isPgstreamEnabled } from "../utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups action tables by their "action" property
|
* Groups action tables by their "action" property
|
||||||
@@ -102,15 +103,27 @@ export async function syncLinks(
|
|||||||
executeSafe,
|
executeSafe,
|
||||||
directory,
|
directory,
|
||||||
container,
|
container,
|
||||||
|
concurrency,
|
||||||
}: {
|
}: {
|
||||||
executeSafe: boolean
|
executeSafe: boolean
|
||||||
executeAll: boolean
|
executeAll: boolean
|
||||||
directory: string
|
directory: string
|
||||||
container: MedusaContainer
|
container: MedusaContainer
|
||||||
|
concurrency?: number
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
||||||
|
|
||||||
|
// Check if pgstream is enabled - if so, force concurrency to 1
|
||||||
|
const pgstreamEnabled = await isPgstreamEnabled(container)
|
||||||
|
if (pgstreamEnabled) {
|
||||||
|
concurrency = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(concurrency)) {
|
||||||
|
process.env.DB_MIGRATION_CONCURRENCY = String(concurrency)
|
||||||
|
}
|
||||||
|
|
||||||
const planner = await medusaAppLoader.getLinksExecutionPlanner()
|
const planner = await medusaAppLoader.getLinksExecutionPlanner()
|
||||||
|
|
||||||
logger.info("Syncing links...")
|
logger.info("Syncing links...")
|
||||||
@@ -192,7 +205,12 @@ export async function syncLinks(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const main = async function ({ directory, executeSafe, executeAll }) {
|
const main = async function ({
|
||||||
|
directory,
|
||||||
|
executeSafe,
|
||||||
|
executeAll,
|
||||||
|
concurrency,
|
||||||
|
}) {
|
||||||
const container = await initializeContainer(directory)
|
const container = await initializeContainer(directory)
|
||||||
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
||||||
|
|
||||||
@@ -218,6 +236,7 @@ const main = async function ({ directory, executeSafe, executeAll }) {
|
|||||||
executeSafe,
|
executeSafe,
|
||||||
directory,
|
directory,
|
||||||
container,
|
container,
|
||||||
|
concurrency,
|
||||||
})
|
})
|
||||||
process.exit()
|
process.exit()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -19,3 +19,21 @@ export async function ensureDbExists(container: MedusaContainer) {
|
|||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function isPgstreamEnabled(
|
||||||
|
container: MedusaContainer
|
||||||
|
): Promise<boolean> {
|
||||||
|
const pgConnection = container.resolve(
|
||||||
|
ContainerRegistrationKeys.PG_CONNECTION
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await pgConnection.raw(
|
||||||
|
"SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'pgstream'"
|
||||||
|
)
|
||||||
|
return result.rows.length > 0
|
||||||
|
} catch (error) {
|
||||||
|
// If there's an error checking, assume pgstream is not enabled
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,18 +6,18 @@ import {
|
|||||||
PlannerActionLinkDescriptor,
|
PlannerActionLinkDescriptor,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
|
|
||||||
import {
|
|
||||||
arrayDifference,
|
|
||||||
DALUtils,
|
|
||||||
ModulesSdkUtils,
|
|
||||||
normalizeMigrationSQL,
|
|
||||||
promiseAll,
|
|
||||||
} from "@medusajs/framework/utils"
|
|
||||||
import { EntitySchema, MikroORM } from "@medusajs/framework/mikro-orm/core"
|
import { EntitySchema, MikroORM } from "@medusajs/framework/mikro-orm/core"
|
||||||
import {
|
import {
|
||||||
DatabaseSchema,
|
DatabaseSchema,
|
||||||
PostgreSqlDriver,
|
PostgreSqlDriver,
|
||||||
} from "@medusajs/framework/mikro-orm/postgresql"
|
} from "@medusajs/framework/mikro-orm/postgresql"
|
||||||
|
import {
|
||||||
|
arrayDifference,
|
||||||
|
DALUtils,
|
||||||
|
executeWithConcurrency,
|
||||||
|
ModulesSdkUtils,
|
||||||
|
normalizeMigrationSQL,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
import { generateEntity } from "../utils"
|
import { generateEntity } from "../utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -195,7 +195,7 @@ export class MigrationsExecutionPlanner implements ILinkMigrationsPlanner {
|
|||||||
|
|
||||||
INSERT INTO "${
|
INSERT INTO "${
|
||||||
this.tableName
|
this.tableName
|
||||||
}" (table_name, link_descriptor) VALUES (?, ?);
|
}" (table_name, link_descriptor) VALUES (?, ?) ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
${sql}
|
${sql}
|
||||||
`,
|
`,
|
||||||
@@ -513,22 +513,30 @@ export class MigrationsExecutionPlanner implements ILinkMigrationsPlanner {
|
|||||||
async executePlan(actionPlan: LinkMigrationsPlannerAction[]): Promise<void> {
|
async executePlan(actionPlan: LinkMigrationsPlannerAction[]): Promise<void> {
|
||||||
const orm = await this.createORM()
|
const orm = await this.createORM()
|
||||||
|
|
||||||
await promiseAll(
|
try {
|
||||||
actionPlan.map(async (action) => {
|
const concurrency = parseInt(process.env.DB_MIGRATION_CONCURRENCY ?? "1")
|
||||||
switch (action.action) {
|
await executeWithConcurrency(
|
||||||
case "delete":
|
actionPlan.map((action) => {
|
||||||
return await this.dropLinkTable(orm, action.tableName)
|
return async () => {
|
||||||
case "create":
|
switch (action.action) {
|
||||||
return await this.createLinkTable(orm, action)
|
case "delete":
|
||||||
case "update":
|
return await this.dropLinkTable(orm, action.tableName)
|
||||||
const sql = `SET LOCAL search_path TO "${this.#schema}"; \n\n${
|
case "create":
|
||||||
action.sql
|
return await this.createLinkTable(orm, action)
|
||||||
}`
|
case "update":
|
||||||
return await orm.em.getDriver().getConnection().execute(sql)
|
const sql = `SET LOCAL search_path TO "${this.#schema}"; \n\n${
|
||||||
default:
|
action.sql
|
||||||
return
|
}`
|
||||||
}
|
return await orm.em.getDriver().getConnection().execute(sql)
|
||||||
})
|
default:
|
||||||
).finally(() => orm.close(true))
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
concurrency
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
await orm.close(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user