Skip to content

Commit 9d8ed70

Browse files
feat(cli): servers and workers in cluster mode (medusajs#13601)
* feat(cli): servers and workers in cluster mode * allow percentage
1 parent cbfe0a4 commit 9d8ed70

File tree

3 files changed

+86
-8
lines changed

3 files changed

+86
-8
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@medusajs/cli": patch
3+
"@medusajs/medusa": patch
4+
---
5+
6+
feat(cli): servers and workers in cluster mode

packages/cli/medusa-cli/src/create-cli.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -416,9 +416,19 @@ function buildLocalCommands(cli, isLocalProject) {
416416
: `Set port. Defaults to ${defaultPort}`,
417417
})
418418
.option(`cluster`, {
419-
type: `number`,
419+
type: `string`,
420420
describe:
421-
"Start the Node.js server in cluster mode. You can specify the number of cpus to use, which defaults to (env.CPUS)",
421+
"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.",
422+
})
423+
.option("workers", {
424+
type: "string",
425+
default: "0",
426+
describe: "Number of worker processes in cluster mode or a percentage of cluster size (e.g., 25%).",
427+
})
428+
.option("servers", {
429+
type: "string",
430+
default: "0",
431+
describe: "Number of server processes in cluster mode or a percentage of cluster size (e.g., 25%).",
422432
}),
423433
handler: handlerP(
424434
getCommandHandler(`start`, (args, cmd) => {

packages/medusa/src/commands/start.ts

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,32 @@ const EVERY_SIXTH_HOUR = "0 */6 * * *"
2626
const CRON_SCHEDULE = EVERY_SIXTH_HOUR
2727
const INSTRUMENTATION_FILE = "instrumentation"
2828

29+
function parseValueOrPercentage(value: string, base: number): number {
30+
if (typeof value !== "string") {
31+
throw new Error(`Invalid value: ${value}. Must be a string.`)
32+
}
33+
34+
const trimmed = value.trim()
35+
if (trimmed.endsWith("%")) {
36+
const percent = parseFloat(trimmed.slice(0, -1))
37+
if (isNaN(percent)) {
38+
throw new Error(`Invalid percentage: ${value}`)
39+
}
40+
if (percent < 0 || percent > 100) {
41+
throw new Error(`Percentage must be between 0 and 100: ${value}`)
42+
}
43+
return Math.round((percent / 100) * base)
44+
} else {
45+
const num = parseInt(trimmed, 10)
46+
if (isNaN(num) || num < 0) {
47+
throw new Error(
48+
`Invalid number: ${value}. Must be a non-negative integer.`
49+
)
50+
}
51+
return num
52+
}
53+
}
54+
2955
/**
3056
* Imports the "instrumentation.js" file from the root of the
3157
* directory and invokes the register function. The existence
@@ -142,9 +168,30 @@ async function start(args: {
142168
host?: string
143169
port?: number
144170
types?: boolean
145-
cluster?: number
171+
cluster?: string
172+
workers?: string
173+
servers?: string
146174
}) {
147-
const { port = 9000, host, directory, types } = args
175+
const {
176+
port = 9000,
177+
host,
178+
directory,
179+
types,
180+
cluster: clusterSize,
181+
workers,
182+
servers,
183+
} = args
184+
185+
const maxCpus = os.cpus().length
186+
const clusterSizeNum = clusterSize
187+
? parseValueOrPercentage(clusterSize, maxCpus)
188+
: maxCpus
189+
const serversCount = servers
190+
? parseValueOrPercentage(servers, clusterSizeNum)
191+
: 0
192+
const workersCount = workers
193+
? parseValueOrPercentage(workers, clusterSizeNum)
194+
: 0
148195

149196
async function internalStart(generateTypes: boolean) {
150197
track("CLI_START")
@@ -261,21 +308,32 @@ async function start(args: {
261308
* cluster mode
262309
*/
263310
if ("cluster" in args) {
264-
const maxCpus = os.cpus().length
265-
const cpus = args.cluster ?? maxCpus
311+
const cpus = clusterSizeNum
312+
const numCPUs = Math.min(maxCpus, cpus)
313+
314+
if (serversCount + workersCount > numCPUs) {
315+
throw new Error(
316+
`Sum of servers (${serversCount}) and workers (${workersCount}) cannot exceed cluster size (${numCPUs})`
317+
)
318+
}
266319

267320
if (cluster.isPrimary) {
268321
let isShuttingDown = false
269-
const numCPUs = Math.min(maxCpus, cpus)
270322
const killMainProccess = () => process.exit(0)
271323
const gracefulShutDown = () => {
272324
isShuttingDown = true
273325
}
274326

275327
for (let index = 0; index < numCPUs; index++) {
276328
const worker = cluster.fork()
329+
let workerMode: "server" | "worker" | "shared" = "shared"
330+
if (index < serversCount) {
331+
workerMode = "server"
332+
} else if (index < serversCount + workersCount) {
333+
workerMode = "worker"
334+
}
277335
worker.on("online", () => {
278-
worker.send({ index })
336+
worker.send({ index, workerMode })
279337
})
280338
}
281339

@@ -291,6 +349,10 @@ async function start(args: {
291349
process.on("SIGINT", gracefulShutDown)
292350
} else {
293351
process.on("message", async (msg: any) => {
352+
if (msg.workerMode) {
353+
process.env.MEDUSA_WORKER_MODE = msg.workerMode
354+
}
355+
294356
if (msg.index > 0) {
295357
process.env.PLUGIN_ADMIN_UI_SKIP_CACHE = "true"
296358
}

0 commit comments

Comments
 (0)