Skip to content

Commit 10ba5f7

Browse files
authored
Revert static worker refactor (#56767)
The changes to this worker from #55841 seem to also cause strange behavior where the process doesn't exit despite sending an exit signal. Reverting until we can spend the time to investigate what the issue is. [slack x-ref](https://vercel.slack.com/archives/C03S8ED1DKM/p1697129702263269) [slack x-ref](https://vercel.slack.com/archives/C056QDZARTM/p1697149427099799?thread_ts=1696351087.736439&cid=C056QDZARTM)
1 parent 786ef25 commit 10ba5f7

File tree

3 files changed

+83
-148
lines changed

3 files changed

+83
-148
lines changed

packages/next/src/build/index.ts

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import type { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
44
import type { ExportPathMap, NextConfigComplete } from '../server/config-shared'
55
import type { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
66
import type { ActionManifest } from './webpack/plugins/flight-client-entry-plugin'
7-
import type {
8-
ExportAppOptions,
9-
ExportAppWorker,
10-
ExportPageInput,
11-
} from '../export/types'
7+
import type { ExportAppOptions, ExportAppWorker } from '../export/types'
128
import type { Revalidate } from '../server/lib/revalidate'
139

1410
import '../lib/setup-exception-listeners'
@@ -1209,16 +1205,7 @@ export default async function build(
12091205
) {
12101206
let infoPrinted = false
12111207

1212-
return Worker.create<
1213-
Pick<
1214-
typeof import('./worker'),
1215-
| 'hasCustomGetInitialProps'
1216-
| 'isPageStatic'
1217-
| 'getDefinedNamedExports'
1218-
| 'exportPage'
1219-
>,
1220-
[ExportPageInput]
1221-
>(staticWorkerPath, {
1208+
return new Worker(staticWorkerPath, {
12221209
timeout: timeout * 1000,
12231210
onRestart: (method, [arg], attempts) => {
12241211
if (method === 'exportPage') {
@@ -1265,7 +1252,14 @@ export default async function build(
12651252
'getDefinedNamedExports',
12661253
'exportPage',
12671254
],
1268-
})
1255+
}) as Worker &
1256+
Pick<
1257+
typeof import('./worker'),
1258+
| 'hasCustomGetInitialProps'
1259+
| 'isPageStatic'
1260+
| 'getDefinedNamedExports'
1261+
| 'exportPage'
1262+
>
12691263
}
12701264

12711265
let CacheHandler: any

packages/next/src/export/index.ts

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type {
33
ExportAppOptions,
44
ExportWorker,
55
WorkerRenderOptsPartial,
6-
ExportPageInput,
76
} from './types'
87
import type { PrerenderManifest } from '../build'
98
import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
@@ -174,32 +173,29 @@ function setupWorkers(
174173

175174
let infoPrinted = false
176175

177-
const worker = Worker.create<typeof import('./worker'), [ExportPageInput]>(
178-
require.resolve('./worker'),
179-
{
180-
timeout: timeout * 1000,
181-
onRestart: (_method, [{ path }], attempts) => {
182-
if (attempts >= 3) {
183-
throw new ExportError(
184-
`Static page generation for ${path} is still timing out after 3 attempts. See more info here https://nextjs.org/docs/messages/static-page-generation-timeout`
185-
)
186-
}
176+
const worker = new Worker(require.resolve('./worker'), {
177+
timeout: timeout * 1000,
178+
onRestart: (_method, [{ path }], attempts) => {
179+
if (attempts >= 3) {
180+
throw new ExportError(
181+
`Static page generation for ${path} is still timing out after 3 attempts. See more info here https://nextjs.org/docs/messages/static-page-generation-timeout`
182+
)
183+
}
184+
Log.warn(
185+
`Restarted static page generation for ${path} because it took more than ${timeout} seconds`
186+
)
187+
if (!infoPrinted) {
187188
Log.warn(
188-
`Restarted static page generation for ${path} because it took more than ${timeout} seconds`
189+
'See more info here https://nextjs.org/docs/messages/static-page-generation-timeout'
189190
)
190-
if (!infoPrinted) {
191-
Log.warn(
192-
'See more info here https://nextjs.org/docs/messages/static-page-generation-timeout'
193-
)
194-
infoPrinted = true
195-
}
196-
},
197-
maxRetries: 0,
198-
numWorkers: threads,
199-
enableWorkerThreads: nextConfig.experimental.workerThreads,
200-
exposedMethods: ['default'],
201-
}
202-
)
191+
infoPrinted = true
192+
}
193+
},
194+
maxRetries: 0,
195+
numWorkers: threads,
196+
enableWorkerThreads: nextConfig.experimental.workerThreads,
197+
exposedMethods: ['default'],
198+
}) as Worker & typeof import('./worker')
203199

204200
return {
205201
pages: worker.default,

packages/next/src/lib/worker.ts

Lines changed: 52 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import type { ChildProcess } from 'child_process'
22
import { Worker as JestWorker } from 'next/dist/compiled/jest-worker'
33
import { getNodeOptionsWithoutInspect } from '../server/lib/utils'
4-
5-
// We need this as we're using `Promise.withResolvers` which is not available in the node typings
6-
import '../lib/polyfill-promise-with-resolvers'
7-
84
type FarmOptions = ConstructorParameters<typeof JestWorker>[1]
95

106
const RESTARTED = Symbol('restarted')
@@ -17,58 +13,45 @@ const cleanupWorkers = (worker: JestWorker) => {
1713
}
1814
}
1915

20-
type Options<
21-
T extends object = object,
22-
Args extends any[] = any[]
23-
> = FarmOptions & {
24-
timeout?: number
25-
onRestart?: (method: string, args: Args, attempts: number) => void
26-
exposedMethods: ReadonlyArray<keyof T>
27-
enableWorkerThreads?: boolean
28-
}
29-
30-
export class Worker<T extends object = object, Args extends any[] = any[]> {
31-
private _worker?: JestWorker
16+
export class Worker {
17+
private _worker: JestWorker | undefined
3218

33-
/**
34-
* Creates a new worker with the correct typings associated with the selected
35-
* methods.
36-
*/
37-
public static create<T extends object, Args extends any[] = any[]>(
19+
constructor(
3820
workerPath: string,
39-
options: Options<T, Args>
40-
): Worker<T, Args> & T {
41-
return new Worker(workerPath, options) as Worker<T, Args> & T
42-
}
43-
44-
constructor(workerPath: string, options: Options<T, Args>) {
21+
options: FarmOptions & {
22+
timeout?: number
23+
onRestart?: (method: string, args: any[], attempts: number) => void
24+
exposedMethods: ReadonlyArray<string>
25+
enableWorkerThreads?: boolean
26+
}
27+
) {
4528
let { timeout, onRestart, ...farmOptions } = options
4629

4730
let restartPromise: Promise<typeof RESTARTED>
4831
let resolveRestartPromise: (arg: typeof RESTARTED) => void
4932
let activeTasks = 0
5033

34+
this._worker = undefined
35+
5136
const createWorker = () => {
52-
const worker = new JestWorker(workerPath, {
37+
this._worker = new JestWorker(workerPath, {
5338
...farmOptions,
5439
forkOptions: {
5540
...farmOptions.forkOptions,
5641
env: {
57-
...farmOptions.forkOptions?.env,
42+
...((farmOptions.forkOptions?.env || {}) as any),
5843
...process.env,
59-
// We don't pass down NODE_OPTIONS as it can lead to extra memory
60-
// usage,
44+
// we don't pass down NODE_OPTIONS as it can
45+
// extra memory usage
6146
NODE_OPTIONS: getNodeOptionsWithoutInspect()
6247
.replace(/--max-old-space-size=[\d]{1,}/, '')
6348
.trim(),
64-
},
65-
stdio: 'inherit',
49+
} as any,
6650
},
67-
})
68-
69-
const { promise, resolve } = Promise.withResolvers<typeof RESTARTED>()
70-
restartPromise = promise
71-
resolveRestartPromise = resolve
51+
}) as JestWorker
52+
restartPromise = new Promise(
53+
(resolve) => (resolveRestartPromise = resolve)
54+
)
7255

7356
/**
7457
* Jest Worker has two worker types, ChildProcessWorker (uses child_process) and NodeThreadWorker (uses worker_threads)
@@ -80,14 +63,11 @@ export class Worker<T extends object = object, Args extends any[] = any[]> {
8063
* But this property is not available in NodeThreadWorker, so we need to check if we are using ChildProcessWorker
8164
*/
8265
if (!farmOptions.enableWorkerThreads) {
83-
const poolWorkers: { _child?: ChildProcess }[] =
84-
// @ts-expect-error - we're accessing a private property
85-
worker._workerPool?._workers ?? []
86-
87-
for (const poolWorker of poolWorkers) {
88-
if (!poolWorker._child) continue
89-
90-
poolWorker._child.once('exit', (code, signal) => {
66+
for (const worker of ((this._worker as any)._workerPool?._workers ||
67+
[]) as {
68+
_child?: ChildProcess
69+
}[]) {
70+
worker._child?.on('exit', (code, signal) => {
9171
// log unexpected exit if .end() wasn't called
9272
if ((code || (signal && signal !== 'SIGINT')) && this._worker) {
9373
console.error(
@@ -98,22 +78,16 @@ export class Worker<T extends object = object, Args extends any[] = any[]> {
9878
}
9979
}
10080

101-
return worker
81+
this._worker.getStdout().pipe(process.stdout)
82+
this._worker.getStderr().pipe(process.stderr)
10283
}
103-
104-
// Create the first worker.
105-
this._worker = createWorker()
84+
createWorker()
10685

10786
const onHanging = () => {
10887
const worker = this._worker
10988
if (!worker) return
110-
111-
// Grab the current restart promise, and create a new worker.
11289
const resolve = resolveRestartPromise
113-
this._worker = createWorker()
114-
115-
// Once the old worker is ended, resolve the restart promise to signal to
116-
// any active tasks that the worker had to be restarted.
90+
createWorker()
11791
worker.end().then(() => {
11892
resolve(RESTARTED)
11993
})
@@ -122,62 +96,33 @@ export class Worker<T extends object = object, Args extends any[] = any[]> {
12296
let hangingTimer: NodeJS.Timeout | false = false
12397

12498
const onActivity = () => {
125-
// If there was an active hanging timer, clear it.
12699
if (hangingTimer) clearTimeout(hangingTimer)
127-
128-
// If there are no active tasks, we don't need to start a new hanging
129-
// timer.
130-
if (activeTasks === 0) return
131-
132-
hangingTimer = setTimeout(onHanging, timeout)
100+
hangingTimer = activeTasks > 0 && setTimeout(onHanging, timeout)
133101
}
134102

135-
const wrapMethodWithTimeout =
136-
(methodName: keyof T) =>
137-
async (...args: Args) => {
138-
activeTasks++
139-
140-
try {
141-
let attempts = 0
142-
for (;;) {
143-
// Mark that we're doing work, we want to ensure that if the worker
144-
// halts for any reason, we restart it.
145-
onActivity()
146-
147-
const result = await Promise.race([
148-
// Either we'll get the result from the worker, or we'll get the
149-
// restart promise to fire.
150-
// @ts-expect-error - we're grabbing a dynamic method on the worker
151-
this._worker[methodName](...args),
152-
restartPromise,
153-
])
154-
155-
// If the result anything besides `RESTARTED`, we can return it, as
156-
// it's the actual result from the worker.
157-
if (result !== RESTARTED) {
158-
return result
103+
for (const method of farmOptions.exposedMethods) {
104+
if (method.startsWith('_')) continue
105+
;(this as any)[method] = timeout
106+
? // eslint-disable-next-line no-loop-func
107+
async (...args: any[]) => {
108+
activeTasks++
109+
try {
110+
let attempts = 0
111+
for (;;) {
112+
onActivity()
113+
const result = await Promise.race([
114+
(this._worker as any)[method](...args),
115+
restartPromise,
116+
])
117+
if (result !== RESTARTED) return result
118+
if (onRestart) onRestart(method, args, ++attempts)
119+
}
120+
} finally {
121+
activeTasks--
122+
onActivity()
159123
}
160-
161-
// Otherwise, we'll need to restart the worker, and try again.
162-
if (onRestart) onRestart(methodName.toString(), args, ++attempts)
163124
}
164-
} finally {
165-
activeTasks--
166-
onActivity()
167-
}
168-
}
169-
170-
for (const name of farmOptions.exposedMethods) {
171-
if (name.startsWith('_')) continue
172-
173-
// @ts-expect-error - we're grabbing a dynamic method on the worker
174-
let method = this._worker[name].bind(this._worker)
175-
if (timeout) {
176-
method = wrapMethodWithTimeout(name)
177-
}
178-
179-
// @ts-expect-error - we're dynamically creating methods
180-
this[name] = method
125+
: (this._worker as any)[method].bind(this._worker)
181126
}
182127
}
183128

0 commit comments

Comments
 (0)