Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use a shared worker pool for collecting page data and static page generation #27924

Merged
merged 8 commits into from Aug 12, 2021
3 changes: 2 additions & 1 deletion errors/static-page-generation-timeout.md
Expand Up @@ -9,10 +9,11 @@ When restarted it will retry all uncompleted jobs, but if a job was unsuccessful
#### Possible Ways to Fix It

- Make sure that there is no infinite loop during execution.
- Make sure all Promises in `getStaticProps` `resolve` or `reject` correctly.
- Make sure all Promises in `getStaticPaths`/`getStaticProps` `resolve` or `reject` correctly.
- Avoid very long timeouts for network requests.
- Increase the timeout by changing the `experimental.staticPageGenerationTimeout` configuration option (default `60` in seconds).

### Useful Links

- [`getStaticPaths`](https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation)
- [`getStaticProps`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation)
114 changes: 72 additions & 42 deletions packages/next/build/index.ts
Expand Up @@ -91,8 +91,6 @@ import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import { isWebpack5 } from 'next/dist/compiled/webpack/webpack'
import { NextConfigComplete } from '../server/config-shared'

const staticCheckWorker = require.resolve('./utils')

export type SsgRoute = {
initialRevalidateSeconds: number | false
srcRoute: string | null
Expand Down Expand Up @@ -670,6 +668,62 @@ export default async function build(
await promises.readFile(buildManifestPath, 'utf8')
) as BuildManifest

const timeout = config.experimental.staticPageGenerationTimeout || 0
const sharedPool = config.experimental.sharedPool || false
const staticWorker = sharedPool
? require.resolve('./worker')
: require.resolve('./utils')
let infoPrinted = false
const staticWorkers = new Worker(staticWorker, {
timeout: timeout * 1000,
onRestart: (method, [arg], attempts) => {
if (method === 'exportPage') {
const { path: pagePath } = arg
if (attempts >= 3) {
throw new Error(
`Static page generation for ${pagePath} is still timing out after 3 attempts. See more info here https://nextjs.org/docs/messages/static-page-generation-timeout`
)
}
Log.warn(
`Restarted static page genertion for ${pagePath} because it took more than ${timeout} seconds`
)
} else {
const pagePath = arg
if (attempts >= 2) {
throw new Error(
`Collecting page data for ${pagePath} is still timing out after 2 attempts. See more info here https://nextjs.org/docs/messages/page-data-collection-timeout`
)
}
Log.warn(
`Restarted collecting page data for ${pagePath} because it took more than ${timeout} seconds`
)
}
if (!infoPrinted) {
Log.warn(
'See more info here https://nextjs.org/docs/messages/static-page-generation-timeout'
)
infoPrinted = true
}
},
numWorkers: config.experimental.cpus,
enableWorkerThreads: config.experimental.workerThreads,
exposedMethods: sharedPool
? [
'hasCustomGetInitialProps',
'isPageStatic',
'getNamedExports',
'exportPage',
]
: ['hasCustomGetInitialProps', 'isPageStatic', 'getNamedExports'],
}) as Worker &
Pick<
typeof import('./worker'),
| 'hasCustomGetInitialProps'
| 'isPageStatic'
| 'getNamedExports'
| 'exportPage'
>

const analysisBegin = process.hrtime()

const staticCheckSpan = nextBuildSpan.traceChild('static-check')
Expand All @@ -682,39 +736,6 @@ export default async function build(
} = await staticCheckSpan.traceAsyncFn(async () => {
process.env.NEXT_PHASE = PHASE_PRODUCTION_BUILD

const timeout = config.experimental.pageDataCollectionTimeout || 0
let infoPrinted = false
const staticCheckWorkers = new Worker(staticCheckWorker, {
timeout: timeout * 1000,
onRestart: (_method, [pagePath], attempts) => {
if (attempts >= 2) {
throw new Error(
`Collecting page data for ${pagePath} is still timing out after 2 attempts. See more info here https://nextjs.org/docs/messages/page-data-collection-timeout`
)
}
Log.warn(
`Restarted collecting page data for ${pagePath} because it took more than ${timeout} seconds`
)
if (!infoPrinted) {
Log.warn(
'See more info here https://nextjs.org/docs/messages/page-data-collection-timeout'
)
infoPrinted = true
}
},
numWorkers: config.experimental.cpus,
enableWorkerThreads: config.experimental.workerThreads,
exposedMethods: [
'hasCustomGetInitialProps',
'isPageStatic',
'getNamedExports',
],
}) as Worker &
Pick<
typeof import('./utils'),
'hasCustomGetInitialProps' | 'isPageStatic' | 'getNamedExports'
>

const runtimeEnvConfig = {
publicRuntimeConfig: config.publicRuntimeConfig,
serverRuntimeConfig: config.serverRuntimeConfig,
Expand All @@ -726,7 +747,7 @@ export default async function build(
const errorPageHasCustomGetInitialProps = nonStaticErrorPageSpan.traceAsyncFn(
async () =>
hasCustomErrorPage &&
(await staticCheckWorkers.hasCustomGetInitialProps(
(await staticWorkers.hasCustomGetInitialProps(
'/_error',
distDir,
isLikeServerless,
Expand All @@ -738,7 +759,7 @@ export default async function build(
const errorPageStaticResult = nonStaticErrorPageSpan.traceAsyncFn(
async () =>
hasCustomErrorPage &&
staticCheckWorkers.isPageStatic(
staticWorkers.isPageStatic(
'/_error',
distDir,
isLikeServerless,
Expand All @@ -753,15 +774,15 @@ export default async function build(
// from _error instead
const appPageToCheck = isLikeServerless ? '/_error' : '/_app'

const customAppGetInitialPropsPromise = staticCheckWorkers.hasCustomGetInitialProps(
const customAppGetInitialPropsPromise = staticWorkers.hasCustomGetInitialProps(
appPageToCheck,
distDir,
isLikeServerless,
runtimeEnvConfig,
true
)

const namedExportsPromise = staticCheckWorkers.getNamedExports(
const namedExportsPromise = staticWorkers.getNamedExports(
appPageToCheck,
distDir,
isLikeServerless,
Expand Down Expand Up @@ -808,7 +829,7 @@ export default async function build(
'is-page-static'
)
let workerResult = await isPageStaticSpan.traceAsyncFn(() => {
return staticCheckWorkers.isPageStatic(
return staticWorkers.isPageStatic(
page,
distDir,
isLikeServerless,
Expand Down Expand Up @@ -926,7 +947,7 @@ export default async function build(
hasNonStaticErrorPage: nonStaticErrorPage,
}

staticCheckWorkers.end()
if (!sharedPool) staticWorkers.end()
return returnValue
})

Expand Down Expand Up @@ -1082,14 +1103,23 @@ export default async function build(
ssgPages,
additionalSsgPaths
)
const exportApp = require('../export').default
const exportApp: typeof import('../export').default = require('../export')
.default
const exportOptions = {
silent: false,
buildExport: true,
threads: config.experimental.cpus,
pages: combinedPages,
outdir: path.join(distDir, 'export'),
statusMessage: 'Generating static pages',
exportPageWorker: sharedPool
? staticWorkers.exportPage.bind(staticWorkers)
: undefined,
endWorker: sharedPool
? async () => {
await staticWorkers.end()
}
: undefined,
}
const exportConfig: any = {
...config,
Expand Down
2 changes: 2 additions & 0 deletions packages/next/build/worker.ts
@@ -0,0 +1,2 @@
export * from './utils'
export { default as exportPage } from '../export/worker'
61 changes: 38 additions & 23 deletions packages/next/export/index.ts
Expand Up @@ -137,6 +137,8 @@ interface ExportOptions {
pages?: string[]
buildExport?: boolean
statusMessage?: string
exportPageWorker?: typeof import('./worker').default
endWorker?: () => Promise<void>
}

export default async function exportApp(
Expand Down Expand Up @@ -519,29 +521,40 @@ export default async function exportApp(

const timeout = configuration?.experimental.staticPageGenerationTimeout || 0
let infoPrinted = false
const worker = new Worker(require.resolve('./worker'), {
timeout: timeout * 1000,
onRestart: (_method, [{ path }], attempts) => {
if (attempts >= 3) {
throw new Error(
`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`
)
}
Log.warn(
`Restarted static page genertion for ${path} because it took more than ${timeout} seconds`
)
if (!infoPrinted) {
let exportPage: typeof import('./worker').default
let endWorker: () => Promise<void>
if (options.exportPageWorker) {
exportPage = options.exportPageWorker
endWorker = options.endWorker || (() => Promise.resolve())
} else {
const worker = new Worker(require.resolve('./worker'), {
timeout: timeout * 1000,
onRestart: (_method, [{ path }], attempts) => {
if (attempts >= 3) {
throw new Error(
`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`
)
}
Log.warn(
'See more info here https://nextjs.org/docs/messages/static-page-generation-timeout'
`Restarted static page genertion for ${path} because it took more than ${timeout} seconds`
)
infoPrinted = true
}
},
maxRetries: 0,
numWorkers: threads,
enableWorkerThreads: nextConfig.experimental.workerThreads,
exposedMethods: ['default'],
}) as Worker & typeof import('./worker')
if (!infoPrinted) {
Log.warn(
'See more info here https://nextjs.org/docs/messages/static-page-generation-timeout'
)
infoPrinted = true
}
},
maxRetries: 0,
numWorkers: threads,
enableWorkerThreads: nextConfig.experimental.workerThreads,
exposedMethods: ['default'],
}) as Worker & typeof import('./worker')
exportPage = worker.default.bind(worker)
endWorker = async () => {
await worker.end()
}
}

let renderError = false
const errorPaths: string[] = []
Expand All @@ -553,7 +566,7 @@ export default async function exportApp(

return pageExportSpan.traceAsyncFn(async () => {
const pathMap = exportPathMap[path]
const result = await worker.default({
const result = await exportPage({
path,
pathMap,
distDir,
Expand Down Expand Up @@ -604,7 +617,7 @@ export default async function exportApp(
})
)

worker.end()
const endWorkerPromise = endWorker()

// copy prerendered routes to outDir
if (!options.buildExport && prerenderManifest) {
Expand Down Expand Up @@ -681,5 +694,7 @@ export default async function exportApp(
if (telemetry) {
await telemetry.flush()
}

await endWorkerPromise
})
}
4 changes: 2 additions & 2 deletions packages/next/server/config-shared.ts
Expand Up @@ -88,6 +88,7 @@ export type NextConfig = { [key: string]: any } & {
swcMinify?: boolean
swcLoader?: boolean
cpus?: number
sharedPool?: boolean
plugins?: boolean
profiling?: boolean
isrFlushToDisk?: boolean
Expand All @@ -111,7 +112,6 @@ export type NextConfig = { [key: string]: any } & {
craCompat?: boolean
esmExternals?: boolean | 'loose'
staticPageGenerationTimeout?: number
pageDataCollectionTimeout?: number
isrMemoryCacheSize?: number
}
}
Expand Down Expand Up @@ -166,6 +166,7 @@ export const defaultConfig: NextConfig = {
(Number(process.env.CIRCLE_NODE_TOTAL) ||
(os.cpus() || { length: 1 }).length) - 1
),
sharedPool: false,
plugins: false,
profiling: false,
isrFlushToDisk: true,
Expand All @@ -182,7 +183,6 @@ export const defaultConfig: NextConfig = {
craCompat: false,
esmExternals: false,
staticPageGenerationTimeout: 60,
pageDataCollectionTimeout: 60,
// default to 50MB limit
isrMemoryCacheSize: 50 * 1024 * 1024,
},
Expand Down