Skip to content

Commit be5fd6c

Browse files
authored
next-dev: change cluster usage to child process + stabilise inspect port (#45745
Fixes #45122 and #44817 NEXT-447 This PR: - changes the usage of the cluster module in `next dev` to use `child_process` instead. This is mostly the same thing and helps alleviate issues with libraries that rely on `Cluster.isMaster`. - fixes the implementation also to check if the `--inspect` option is used to then manually attribute the correct port, which is whatever the current port is, plus one. With the previous cluster implementation, it always increased automatically every time the server died. - adds back the warning log I had removed by mistake to inform the user that the server is restarting. <!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: --> ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md)
1 parent 484f472 commit be5fd6c

File tree

2 files changed

+104
-57
lines changed

2 files changed

+104
-57
lines changed

packages/next/src/cli/next-dev.ts

Lines changed: 95 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env node
22
import arg from 'next/dist/compiled/arg/index.js'
3-
import { startServer } from '../server/lib/start-server'
3+
import { startServer, WORKER_SELF_EXIT_CODE } from '../server/lib/start-server'
44
import { getPort, printAndExit } from '../server/lib/utils'
55
import * as Log from '../build/output/log'
66
import { startedDevelopmentServer } from '../build/output'
@@ -13,20 +13,23 @@ import type { NextConfig } from '../../types'
1313
import type { NextConfigComplete } from '../server/config-shared'
1414
import { traceGlobals } from '../trace/shared'
1515
import { isIPv6 } from 'net'
16-
import cluster from 'cluster'
16+
import { ChildProcess, fork } from 'child_process'
1717
import { Telemetry } from '../telemetry/storage'
1818
import loadConfig from '../server/config'
1919
import { findPagesDir } from '../lib/find-pages-dir'
2020
import { fileExists } from '../lib/file-exists'
2121
import Watchpack from 'next/dist/compiled/watchpack'
2222
import stripAnsi from 'next/dist/compiled/strip-ansi'
23+
import { warn } from '../build/output/log'
2324

2425
let isTurboSession = false
2526
let sessionStopHandled = false
2627
let sessionStarted = Date.now()
2728
let dir: string
2829
let unwatchConfigFiles: () => void
2930

31+
const isChildProcess = !!process.env.__NEXT_DEV_CHILD_PROCESS
32+
3033
const handleSessionStop = async () => {
3134
if (sessionStopHandled) return
3235
sessionStopHandled = true
@@ -81,7 +84,7 @@ const handleSessionStop = async () => {
8184
process.exit(0)
8285
}
8386

84-
if (cluster.isMaster) {
87+
if (!isChildProcess) {
8588
process.on('SIGINT', handleSessionStop)
8689
process.on('SIGTERM', handleSessionStop)
8790
} else {
@@ -432,16 +435,75 @@ If you cannot make the changes above, but still want to try out\nNext.js v13 wit
432435
// we're using a sub worker to avoid memory leaks. When memory usage exceeds 90%, we kill the worker and restart it.
433436
// this is a temporary solution until we can fix the memory leaks.
434437
// the logic for the worker killing itself is in `packages/next/server/lib/start-server.ts`
435-
if (!process.env.__NEXT_DISABLE_MEMORY_WATCHER && cluster.isMaster) {
438+
if (!process.env.__NEXT_DISABLE_MEMORY_WATCHER && !isChildProcess) {
436439
let config: NextConfig
440+
let childProcess: ChildProcess | null = null
441+
442+
const isDebugging = process.execArgv.some((localArg) =>
443+
localArg.startsWith('--inspect')
444+
)
445+
446+
const isDebuggingWithBrk = process.execArgv.some((localArg) =>
447+
localArg.startsWith('--inspect-brk')
448+
)
449+
450+
const debugPort = (() => {
451+
const debugPortStr = process.execArgv
452+
.find(
453+
(localArg) =>
454+
localArg.startsWith('--inspect') ||
455+
localArg.startsWith('--inspect-brk')
456+
)
457+
?.split('=')[1]
458+
return debugPortStr ? parseInt(debugPortStr, 10) : 9229
459+
})()
460+
461+
if (isDebugging || isDebuggingWithBrk) {
462+
warn(
463+
`the --inspect${
464+
isDebuggingWithBrk ? '-brk' : ''
465+
} option was detected, the Next.js server should be inspected at port ${
466+
debugPort + 1
467+
}.`
468+
)
469+
}
470+
471+
const genExecArgv = () => {
472+
const execArgv = process.execArgv.filter((localArg) => {
473+
return (
474+
!localArg.startsWith('--inspect') &&
475+
!localArg.startsWith('--inspect-brk')
476+
)
477+
})
437478

438-
const setupFork = (env?: Parameters<typeof cluster.fork>[0]) => {
479+
if (isDebugging || isDebuggingWithBrk) {
480+
execArgv.push(
481+
`--inspect${isDebuggingWithBrk ? '-brk' : ''}=${debugPort + 1}`
482+
)
483+
}
484+
485+
return execArgv
486+
}
487+
488+
const setupFork = (env?: NodeJS.ProcessEnv, newDir?: string) => {
439489
const startDir = dir
490+
const [, script, ...nodeArgs] = process.argv
440491
let shouldFilter = false
441-
cluster.fork({
442-
...env,
443-
FORCE_COLOR: '1',
444-
})
492+
childProcess = fork(
493+
newDir ? script.replace(startDir, newDir) : script,
494+
nodeArgs,
495+
{
496+
env: {
497+
...(env ? env : process.env),
498+
FORCE_COLOR: '1',
499+
__NEXT_DEV_CHILD_PROCESS: '1',
500+
},
501+
// @ts-ignore TODO: remove ignore when types are updated
502+
windowsHide: true,
503+
stdio: ['ipc', 'pipe', 'pipe'],
504+
execArgv: genExecArgv(),
505+
}
506+
)
445507

446508
// since errors can start being logged from the fork
447509
// before we detect the project directory rename
@@ -473,40 +535,27 @@ If you cannot make the changes above, but still want to try out\nNext.js v13 wit
473535
process[fd].write(chunk)
474536
}
475537

476-
for (const workerId in cluster.workers) {
477-
cluster.workers[workerId]?.process.stdout?.on('data', (chunk) => {
478-
filterForkErrors(chunk, 'stdout')
479-
})
480-
cluster.workers[workerId]?.process.stderr?.on('data', (chunk) => {
481-
filterForkErrors(chunk, 'stderr')
482-
})
483-
}
484-
}
485-
486-
const handleClusterExit = () => {
487-
const callback = async (worker: cluster.Worker) => {
488-
// ignore if we killed the worker
489-
if ((worker as any).killed) return
538+
childProcess?.stdout?.on('data', (chunk) => {
539+
filterForkErrors(chunk, 'stdout')
540+
})
541+
childProcess?.stderr?.on('data', (chunk) => {
542+
filterForkErrors(chunk, 'stderr')
543+
})
490544

491-
// TODO: we should track how many restarts are
492-
// occurring and how long in-between them
493-
if (worker.exitedAfterDisconnect) {
545+
const callback = async (code: number | null) => {
546+
if (code === WORKER_SELF_EXIT_CODE) {
494547
setupFork()
495548
} else if (!sessionStopHandled) {
496549
await handleSessionStop()
497550
process.exit(1)
498551
}
499552
}
500-
cluster.addListener('exit', callback)
501-
return () => cluster.removeListener('exit', callback)
553+
childProcess?.addListener('exit', callback)
554+
return () => childProcess?.removeListener('exit', callback)
502555
}
503-
let clusterExitUnsub = handleClusterExit()
504-
// x-ref: https://nodejs.org/api/cluster.html#clustersettings
505-
// @ts-expect-error type is incorrect
506-
cluster.settings.windowsHide = true
507-
cluster.settings.stdio = ['ipc', 'pipe', 'pipe']
508556

509-
setupFork()
557+
let childProcessExitUnsub = setupFork()
558+
510559
config = await loadConfig(
511560
PHASE_DEVELOPMENT_SERVER,
512561
dir,
@@ -516,27 +565,19 @@ If you cannot make the changes above, but still want to try out\nNext.js v13 wit
516565
)
517566

518567
const handleProjectDirRename = (newDir: string) => {
519-
clusterExitUnsub()
520-
521-
for (const workerId in cluster.workers) {
522-
try {
523-
// @ts-expect-error custom field
524-
cluster.workers[workerId].killed = true
525-
cluster.workers[workerId]!.process.kill('SIGKILL')
526-
} catch (_) {}
527-
}
568+
childProcessExitUnsub()
569+
childProcess?.kill()
528570
process.chdir(newDir)
529-
// @ts-expect-error type is incorrect
530-
cluster.settings.cwd = newDir
531-
cluster.settings.exec = cluster.settings.exec?.replace(dir, newDir)
532-
setupFork({
533-
...Object.keys(process.env).reduce((newEnv, key) => {
534-
newEnv[key] = process.env[key]?.replace(dir, newDir)
535-
return newEnv
536-
}, {} as typeof process.env),
537-
NEXT_PRIVATE_DEV_DIR: newDir,
538-
})
539-
clusterExitUnsub = handleClusterExit()
571+
childProcessExitUnsub = setupFork(
572+
{
573+
...Object.keys(process.env).reduce((newEnv, key) => {
574+
newEnv[key] = process.env[key]?.replace(dir, newDir)
575+
return newEnv
576+
}, {} as typeof process.env),
577+
NEXT_PRIVATE_DEV_DIR: newDir,
578+
},
579+
newDir
580+
)
540581
}
541582
const parentDir = path.join('/', dir, '..')
542583
const watchedEntryLength = parentDir.split('/').length + 1

packages/next/src/server/lib/start-server.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import { warn } from '../../build/output/log'
33
import http from 'http'
44
import next from '../next'
55
import { isIPv6 } from 'net'
6-
import cluster from 'cluster'
76
import v8 from 'v8'
7+
const isChildProcess = !!process.env.__NEXT_DEV_CHILD_PROCESS
88

99
interface StartServerOptions extends NextServerOptions {
1010
allowRetry?: boolean
1111
keepAliveTimeout?: number
1212
}
1313

14+
export const WORKER_SELF_EXIT_CODE = 77
15+
1416
const MAXIMUM_HEAP_SIZE_ALLOWED =
1517
(v8.getHeapStatistics().heap_size_limit / 1024 / 1024) * 0.9
1618

@@ -20,10 +22,14 @@ export function startServer(opts: StartServerOptions) {
2022
const server = http.createServer((req, res) => {
2123
return requestHandler(req, res).finally(() => {
2224
if (
23-
cluster.worker &&
25+
isChildProcess &&
2426
process.memoryUsage().heapUsed / 1024 / 1024 > MAXIMUM_HEAP_SIZE_ALLOWED
2527
) {
26-
cluster.worker.kill()
28+
warn(
29+
'The server is running out of memory, restarting to free up memory.'
30+
)
31+
server.close()
32+
process.exit(WORKER_SELF_EXIT_CODE)
2733
}
2834
})
2935
})

0 commit comments

Comments
 (0)