Skip to content

Commit 3a40b9d

Browse files
committed
clean up restarting code
1 parent 2bbe8bb commit 3a40b9d

File tree

3 files changed

+144
-90
lines changed

3 files changed

+144
-90
lines changed

packages/next/errors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,5 +822,6 @@
822822
"821": "Unprocessable request",
823823
"822": "Stack frame resolver not initialized. This is a bug in Next.js.",
824824
"823": "Timeout waiting for error state from frontend. The browser may not be responding to HMR messages.",
825-
"824": "URL is required in MCP error state response. This is a bug in Next.js."
825+
"824": "URL is required in MCP error state response. This is a bug in Next.js.",
826+
"825": "`pipelineInSequentialTasks` should not be called in edge runtime."
826827
}

packages/next/src/server/app-render/app-render-render-utils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,40 @@ export function scheduleInSequentialTasks<R>(
2929
})
3030
}
3131
}
32+
33+
/**
34+
* This is a utility function to make scheduling sequential tasks that run back to back easier.
35+
* We schedule on the same queue (setImmediate) at the same time to ensure no other events can sneak in between.
36+
* The function that runs in the second task gets access to the first tasks's result.
37+
*/
38+
export function pipelineInSequentialTasks<A, B>(
39+
render: () => A,
40+
followup: (a: A) => Promise<B>
41+
): Promise<B> {
42+
if (process.env.NEXT_RUNTIME === 'edge') {
43+
throw new InvariantError(
44+
'`pipelineInSequentialTasks` should not be called in edge runtime.'
45+
)
46+
} else {
47+
return new Promise((resolve, reject) => {
48+
let renderResult: A = undefined!
49+
setImmediate(() => {
50+
try {
51+
renderResult = render()
52+
} catch (err) {
53+
clearImmediate(followupId)
54+
reject(err)
55+
}
56+
})
57+
const followupId = setImmediate(() => {
58+
// if `render` threw, then the `followup` immediate would've been cleared,
59+
// so if we got here, we're guaranteed to have a `renderResult`.
60+
try {
61+
resolve(followup(renderResult))
62+
} catch (err) {
63+
reject(err)
64+
}
65+
})
66+
})
67+
}
68+
}

packages/next/src/server/app-render/app-render.tsx

Lines changed: 105 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,10 @@ import {
168168
prerenderAndAbortInSequentialTasks,
169169
} from './app-render-prerender-utils'
170170
import { printDebugThrownValueForProspectiveRender } from './prospective-render-utils'
171-
import { scheduleInSequentialTasks } from './app-render-render-utils'
171+
import {
172+
pipelineInSequentialTasks,
173+
scheduleInSequentialTasks,
174+
} from './app-render-render-utils'
172175
import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler'
173176
import {
174177
workUnitAsyncStorage,
@@ -2128,19 +2131,28 @@ async function renderToStream(
21282131
_validation?: Promise<React.ReactNode>
21292132
}
21302133

2131-
const getPayload = (): Promise<RSCPayloadWithValidation> =>
2132-
workUnitAsyncStorage.run(
2133-
requestStore,
2134-
getRSCPayload,
2135-
tree,
2136-
ctx,
2137-
res.statusCode === 404
2138-
)
2134+
const [resolveValidation, validationOutlet] = createValidationOutlet()
2135+
2136+
const getPayload = async (): Promise<RSCPayloadWithValidation> => {
2137+
const payload: RSCPayloadWithValidation =
2138+
await workUnitAsyncStorage.run(
2139+
requestStore,
2140+
getRSCPayload,
2141+
tree,
2142+
ctx,
2143+
res.statusCode === 404
2144+
)
2145+
// Placing the validation outlet in the payload is safe
2146+
// even if we end up discarding a render and restarting,
2147+
// because we're not going to wait for the stream to complete,
2148+
// so leaving the validation unresolved is fine.
2149+
payload._validation = validationOutlet
2150+
return payload
2151+
}
21392152

21402153
const environmentName = () =>
21412154
requestStore.prerenderPhase === true ? 'Prerender' : 'Server'
21422155

2143-
const [resolveValidation, validationOutlet] = createValidationOutlet()
21442156
const debugChannel = setReactDebugChannel && createDebugChannel()
21452157

21462158
if (debugChannel) {
@@ -2159,44 +2171,6 @@ async function renderToStream(
21592171
// Try to render the page and see if there's any cache misses.
21602172
// If there are, wait for caches to finish and restart the render.
21612173

2162-
const renderInStages = async (
2163-
serverDebugChannel: DebugChannelServer | undefined,
2164-
signal: AbortSignal | undefined,
2165-
onPrerenderStageEnd: (() => void) | undefined
2166-
) => {
2167-
const rscPayload = await getPayload()
2168-
2169-
// Placing the validation outlet in the payload is safe
2170-
// even if we end up discarding this render and restarting,
2171-
// because it's just an output produced independently.
2172-
rscPayload._validation = validationOutlet
2173-
2174-
return workUnitAsyncStorage.run(
2175-
requestStore,
2176-
scheduleInSequentialTasks,
2177-
() => {
2178-
// Static stage
2179-
requestStore.prerenderPhase = true
2180-
return ComponentMod.renderToReadableStream(
2181-
rscPayload,
2182-
clientReferenceManifest.clientModules,
2183-
{
2184-
onError: serverComponentsErrorHandler,
2185-
environmentName,
2186-
filterStackFrame,
2187-
debugChannel: serverDebugChannel,
2188-
signal,
2189-
}
2190-
)
2191-
},
2192-
() => {
2193-
// Dynamic stage
2194-
requestStore.prerenderPhase = false
2195-
onPrerenderStageEnd?.()
2196-
}
2197-
)
2198-
}
2199-
22002174
// This render might end up being used as a prospective render (if there's cache misses),
22012175
// so we need to set it up for filling caches.
22022176
const cacheSignal = new CacheSignal()
@@ -2216,45 +2190,61 @@ async function renderToStream(
22162190

22172191
const initialRenderReactController = new AbortController()
22182192
// We don't know if we'll use this render, so buffer debug channel writes until we find out.
2219-
const initialRenderServerDebugChannel = debugChannel
2193+
const bufferedServerDebugChannel = debugChannel
22202194
? createBufferedServerDebugChannel()
22212195
: undefined
22222196

2223-
const hadCacheMissInStaticStagePromise =
2224-
createPromiseWithResolvers<boolean>()
2225-
2226-
const reactServerStreamPromise = renderInStages(
2227-
initialRenderServerDebugChannel?.channel,
2228-
initialRenderReactController.signal,
2229-
() => {
2230-
// If all cache reads initiated in the static stage have completed,
2231-
// then either we don't need to fill any caches, or all of them are warm.
2232-
// On the other hand, if we have pending cache reads, then we had a cache miss.
2233-
hadCacheMissInStaticStagePromise.resolve(
2234-
cacheSignal.hasPendingReads()
2197+
const initialRscPayload = await getPayload()
2198+
const maybeInitialServerStream = await workUnitAsyncStorage.run(
2199+
requestStore,
2200+
() =>
2201+
pipelineInSequentialTasks(
2202+
() => {
2203+
// Static stage
2204+
requestStore.prerenderPhase = true
2205+
return ComponentMod.renderToReadableStream(
2206+
initialRscPayload,
2207+
clientReferenceManifest.clientModules,
2208+
{
2209+
onError: serverComponentsErrorHandler,
2210+
environmentName,
2211+
filterStackFrame,
2212+
debugChannel: bufferedServerDebugChannel?.channel,
2213+
signal: initialRenderReactController.signal,
2214+
}
2215+
)
2216+
},
2217+
async (stream) => {
2218+
// Dynamic stage
2219+
// Note: if we had cache misses, things that would've happened statically otherwise
2220+
// may be marked as dynamic instead.
2221+
requestStore.prerenderPhase = false
2222+
2223+
// If all cache reads initiated in the static stage have completed,
2224+
// then all of the necessary caches have to be warm (or there's no caches on the page).
2225+
// On the other hand, if we still have pending cache reads, then we had a cache miss,
2226+
// and the static stage didn't render all the content that it normally would have.
2227+
const hadCacheMiss = cacheSignal.hasPendingReads()
2228+
if (!hadCacheMiss) {
2229+
// No cache misses. We can use the stream as is.
2230+
return stream
2231+
} else {
2232+
// Cache miss. We'll discard this stream, and render again.
2233+
return null
2234+
}
2235+
}
22352236
)
2236-
}
2237-
)
2238-
reactServerStreamPromise.catch((err) =>
2239-
hadCacheMissInStaticStagePromise.reject(err)
22402237
)
22412238

2242-
const hasCacheMissInStaticStage =
2243-
await hadCacheMissInStaticStagePromise.promise
2244-
2245-
if (!hasCacheMissInStaticStage) {
2246-
// No cache misses. Use the stream as is.
2239+
if (maybeInitialServerStream !== null) {
2240+
// No cache misses. We can use the stream as is.
22472241

2248-
// The debug info from this render should be written to the real debug channel.
2249-
if (debugChannel && initialRenderServerDebugChannel) {
2250-
void initialRenderServerDebugChannel.pipeToChannel(
2251-
debugChannel.serverSide
2252-
)
2242+
// Since we're using this render, the debug info we've buffered should be written to the real debug channel.
2243+
if (debugChannel && bufferedServerDebugChannel) {
2244+
void bufferedServerDebugChannel.pipeToChannel(debugChannel.serverSide)
22532245
}
22542246

2255-
reactServerResult = new ReactServerResult(
2256-
await reactServerStreamPromise
2257-
)
2247+
reactServerResult = new ReactServerResult(maybeInitialServerStream)
22582248
} else {
22592249
// Cache miss. We will use the initial render to fill caches, and discard its result.
22602250
// Then, we can render again with warm caches.
@@ -2272,25 +2262,51 @@ async function renderToStream(
22722262
await cacheSignal.cacheReadyInRender()
22732263
initialRenderReactController.abort()
22742264

2275-
// The initial render acted as a prospective render.
2276-
// Now, we need to clear the state we've set up for it and do a regular render.
2265+
//===============================================
2266+
2267+
// The initial render acted as a prospective render to warm the caches.
2268+
// Now, we need to do another render.
2269+
2270+
// TODO(restart-on-cache-miss): we should use a separate request store for this instead
2271+
2272+
// We've filled the caches, so now we can render as usual.
22772273
requestStore.prerenderResumeDataCache = null
22782274
requestStore.renderResumeDataCache = createRenderResumeDataCache(
22792275
prerenderResumeDataCache
22802276
)
22812277
requestStore.cacheSignal = null
22822278

2283-
// We know we'll use this render, so unlike the initial one,
2284-
// it can write into the debug channel directly instead of buffering.
2285-
const finalRenderServerDebugChannel = debugChannel?.serverSide
2279+
// Reset mutable fields.
2280+
requestStore.prerenderPhase = undefined
2281+
requestStore.usedDynamic = undefined
22862282

2287-
reactServerResult = new ReactServerResult(
2288-
await renderInStages(
2289-
finalRenderServerDebugChannel,
2290-
undefined,
2291-
undefined
2292-
)
2283+
const finalRscPayload = await getPayload()
2284+
const finalServerStream = await workUnitAsyncStorage.run(
2285+
requestStore,
2286+
scheduleInSequentialTasks,
2287+
() => {
2288+
// Static stage
2289+
requestStore.prerenderPhase = true
2290+
return ComponentMod.renderToReadableStream(
2291+
finalRscPayload,
2292+
clientReferenceManifest.clientModules,
2293+
{
2294+
onError: serverComponentsErrorHandler,
2295+
environmentName,
2296+
filterStackFrame,
2297+
// We know we'll use this render, so unlike the initial one,
2298+
// it can write into the debug channel directly instead of buffering.
2299+
debugChannel: debugChannel?.serverSide,
2300+
}
2301+
)
2302+
},
2303+
() => {
2304+
// Dynamic stage
2305+
requestStore.prerenderPhase = false
2306+
}
22932307
)
2308+
2309+
reactServerResult = new ReactServerResult(finalServerStream)
22942310
}
22952311

22962312
// TODO(restart-on-cache-miss):

0 commit comments

Comments
 (0)