feat(cli): servers and workers in cluster mode (#13601)

* feat(cli): servers and workers in cluster mode

* allow percentage
This commit is contained in:
Carlos R. L. Rodrigues
2025-09-28 05:06:18 -03:00
committed by GitHub
parent cbfe0a4e95
commit 9d8ed70130
3 changed files with 86 additions and 8 deletions

View File

@@ -0,0 +1,6 @@
---
"@medusajs/cli": patch
"@medusajs/medusa": patch
---
feat(cli): servers and workers in cluster mode

View File

@@ -416,9 +416,19 @@ function buildLocalCommands(cli, isLocalProject) {
: `Set port. Defaults to ${defaultPort}`,
})
.option(`cluster`, {
type: `number`,
type: `string`,
describe:
"Start the Node.js server in cluster mode. You can specify the number of cpus to use, which defaults to (env.CPUS)",
"Start the Node.js server in cluster mode. Specify the number of CPUs to use or a percentage (e.g., 50%). Defaults to the number of available CPUs.",
})
.option("workers", {
type: "string",
default: "0",
describe: "Number of worker processes in cluster mode or a percentage of cluster size (e.g., 25%).",
})
.option("servers", {
type: "string",
default: "0",
describe: "Number of server processes in cluster mode or a percentage of cluster size (e.g., 25%).",
}),
handler: handlerP(
getCommandHandler(`start`, (args, cmd) => {

View File

@@ -26,6 +26,32 @@ const EVERY_SIXTH_HOUR = "0 */6 * * *"
const CRON_SCHEDULE = EVERY_SIXTH_HOUR
const INSTRUMENTATION_FILE = "instrumentation"
function parseValueOrPercentage(value: string, base: number): number {
if (typeof value !== "string") {
throw new Error(`Invalid value: ${value}. Must be a string.`)
}
const trimmed = value.trim()
if (trimmed.endsWith("%")) {
const percent = parseFloat(trimmed.slice(0, -1))
if (isNaN(percent)) {
throw new Error(`Invalid percentage: ${value}`)
}
if (percent < 0 || percent > 100) {
throw new Error(`Percentage must be between 0 and 100: ${value}`)
}
return Math.round((percent / 100) * base)
} else {
const num = parseInt(trimmed, 10)
if (isNaN(num) || num < 0) {
throw new Error(
`Invalid number: ${value}. Must be a non-negative integer.`
)
}
return num
}
}
/**
* Imports the "instrumentation.js" file from the root of the
* directory and invokes the register function. The existence
@@ -142,9 +168,30 @@ async function start(args: {
host?: string
port?: number
types?: boolean
cluster?: number
cluster?: string
workers?: string
servers?: string
}) {
const { port = 9000, host, directory, types } = args
const {
port = 9000,
host,
directory,
types,
cluster: clusterSize,
workers,
servers,
} = args
const maxCpus = os.cpus().length
const clusterSizeNum = clusterSize
? parseValueOrPercentage(clusterSize, maxCpus)
: maxCpus
const serversCount = servers
? parseValueOrPercentage(servers, clusterSizeNum)
: 0
const workersCount = workers
? parseValueOrPercentage(workers, clusterSizeNum)
: 0
async function internalStart(generateTypes: boolean) {
track("CLI_START")
@@ -261,12 +308,17 @@ async function start(args: {
* cluster mode
*/
if ("cluster" in args) {
const maxCpus = os.cpus().length
const cpus = args.cluster ?? maxCpus
const cpus = clusterSizeNum
const numCPUs = Math.min(maxCpus, cpus)
if (serversCount + workersCount > numCPUs) {
throw new Error(
`Sum of servers (${serversCount}) and workers (${workersCount}) cannot exceed cluster size (${numCPUs})`
)
}
if (cluster.isPrimary) {
let isShuttingDown = false
const numCPUs = Math.min(maxCpus, cpus)
const killMainProccess = () => process.exit(0)
const gracefulShutDown = () => {
isShuttingDown = true
@@ -274,8 +326,14 @@ async function start(args: {
for (let index = 0; index < numCPUs; index++) {
const worker = cluster.fork()
let workerMode: "server" | "worker" | "shared" = "shared"
if (index < serversCount) {
workerMode = "server"
} else if (index < serversCount + workersCount) {
workerMode = "worker"
}
worker.on("online", () => {
worker.send({ index })
worker.send({ index, workerMode })
})
}
@@ -291,6 +349,10 @@ async function start(args: {
process.on("SIGINT", gracefulShutDown)
} else {
process.on("message", async (msg: any) => {
if (msg.workerMode) {
process.env.MEDUSA_WORKER_MODE = msg.workerMode
}
if (msg.index > 0) {
process.env.PLUGIN_ADMIN_UI_SKIP_CACHE = "true"
}