Skip to content

Commit

Permalink
Update revalidateTag to batch tags in one request (#65296)
Browse files Browse the repository at this point in the history
As discussed this collects all `revalidateTag` calls and invokes in a
single request to avoid race conditions and overhead from multiple
pending requests.

x-ref: [slack
thread](https://vercel.slack.com/archives/C0676QZBWKS/p1714688045037509?thread_ts=1710902198.529179&cid=C0676QZBWKS)

Closes NEXT-3306
  • Loading branch information
ijjk committed May 3, 2024
1 parent ec74cd9 commit 025f5b6
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 40 deletions.
27 changes: 18 additions & 9 deletions packages/next/src/server/app-render/action-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,12 @@ async function addRevalidationHeader(
requestStore: RequestStore
}
) {
await Promise.all(
Object.values(staticGenerationStore.pendingRevalidates || [])
)
await Promise.all([
staticGenerationStore.incrementalCache?.revalidateTag(
staticGenerationStore.revalidatedTags || []
),
...Object.values(staticGenerationStore.pendingRevalidates || {}),
])

// If a tag was revalidated, the client router needs to invalidate all the
// client router cache as they may be stale. And if a path was revalidated, the
Expand Down Expand Up @@ -480,9 +483,12 @@ export async function handleAction({

if (isFetchAction) {
res.statusCode = 500
await Promise.all(
Object.values(staticGenerationStore.pendingRevalidates || [])
)
await Promise.all([
staticGenerationStore.incrementalCache?.revalidateTag(
staticGenerationStore.revalidatedTags || []
),
...Object.values(staticGenerationStore.pendingRevalidates || {}),
])

const promise = Promise.reject(error)
try {
Expand Down Expand Up @@ -867,9 +873,12 @@ export async function handleAction({

if (isFetchAction) {
res.statusCode = 500
await Promise.all(
Object.values(staticGenerationStore.pendingRevalidates || [])
)
await Promise.all([
staticGenerationStore.incrementalCache?.revalidateTag(
staticGenerationStore.revalidatedTags || []
),
...Object.values(staticGenerationStore.pendingRevalidates || {}),
])
const promise = Promise.reject(err)
try {
// we need to await the promise to trigger the rejection early
Expand Down
9 changes: 6 additions & 3 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1374,9 +1374,12 @@ async function renderToHTMLOrFlightImpl(

// If we have pending revalidates, wait until they are all resolved.
if (staticGenerationStore.pendingRevalidates) {
options.waitUntil = Promise.all(
Object.values(staticGenerationStore.pendingRevalidates)
)
options.waitUntil = Promise.all([
staticGenerationStore.incrementalCache?.revalidateTag(
staticGenerationStore.revalidatedTags || []
),
...Object.values(staticGenerationStore.pendingRevalidates || {}),
])
}

addImplicitTags(staticGenerationStore)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,11 +395,14 @@ export class AppRouteRouteModule extends RouteModule<
context.renderOpts.fetchMetrics =
staticGenerationStore.fetchMetrics

context.renderOpts.waitUntil = Promise.all(
Object.values(
staticGenerationStore.pendingRevalidates || []
)
)
context.renderOpts.waitUntil = Promise.all([
staticGenerationStore.incrementalCache?.revalidateTag(
staticGenerationStore.revalidatedTags || []
),
...Object.values(
staticGenerationStore.pendingRevalidates || {}
),
])

addImplicitTags(staticGenerationStore)
;(context.renderOpts as any).fetchTags =
Expand Down
18 changes: 12 additions & 6 deletions packages/next/src/server/lib/incremental-cache/fetch-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,17 @@ export default class FetchCache implements CacheHandler {
memoryCache?.reset()
}

public async revalidateTag(tag: string) {
public async revalidateTag(
...args: Parameters<CacheHandler['revalidateTag']>
) {
let [tags] = args
tags = typeof tags === 'string' ? [tags] : tags
if (this.debug) {
console.log('revalidateTag', tag)
console.log('revalidateTag', tags)
}

if (!tags.length) return

if (Date.now() < rateLimitedUntil) {
if (this.debug) {
console.log('rate limited ', rateLimitedUntil)
Expand All @@ -140,9 +146,9 @@ export default class FetchCache implements CacheHandler {

try {
const res = await fetch(
`${
this.cacheEndpoint
}/v1/suspense-cache/revalidate?tags=${encodeURIComponent(tag)}`,
`${this.cacheEndpoint}/v1/suspense-cache/revalidate?tags=${tags
.map((tag) => encodeURIComponent(tag))
.join(',')}`,
{
method: 'POST',
headers: this.headers,
Expand All @@ -160,7 +166,7 @@ export default class FetchCache implements CacheHandler {
throw new Error(`Request failed with status ${res.status}.`)
}
} catch (err) {
console.warn(`Failed to revalidate tag ${tag}`, err)
console.warn(`Failed to revalidate tag ${tags}`, err)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,18 @@ export default class FileSystemCache implements CacheHandler {
if (this.debug) console.log('loadTagsManifest', tagsManifest)
}

public async revalidateTag(tag: string) {
public async revalidateTag(
...args: Parameters<CacheHandler['revalidateTag']>
) {
let [tags] = args
tags = typeof tags === 'string' ? [tags] : tags

if (this.debug) {
console.log('revalidateTag', tag)
console.log('revalidateTag', tags)
}

if (tags.length === 0) {
return
}

// we need to ensure the tagsManifest is refreshed
Expand All @@ -118,9 +127,11 @@ export default class FileSystemCache implements CacheHandler {
return
}

const data = tagsManifest.items[tag] || {}
data.revalidatedAt = Date.now()
tagsManifest.items[tag] = data
for (const tag of tags) {
const data = tagsManifest.items[tag] || {}
data.revalidatedAt = Date.now()
tagsManifest.items[tag] = data
}

try {
await this.fs.mkdir(path.dirname(this.tagsManifestPath))
Expand Down
8 changes: 5 additions & 3 deletions packages/next/src/server/lib/incremental-cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ export class CacheHandler {
..._args: Parameters<IncrementalCache['set']>
): Promise<void> {}

public async revalidateTag(_tag: string): Promise<void> {}
public async revalidateTag(
..._args: Parameters<IncrementalCache['revalidateTag']>
): Promise<void> {}

public resetRequestCache(): void {}
}
Expand Down Expand Up @@ -280,7 +282,7 @@ export class IncrementalCache implements IncrementalCacheType {
return unlockNext
}

async revalidateTag(tag: string) {
async revalidateTag(tags: string | string[]): Promise<void> {
if (
process.env.__NEXT_INCREMENTAL_CACHE_IPC_PORT &&
process.env.__NEXT_INCREMENTAL_CACHE_IPC_KEY &&
Expand All @@ -296,7 +298,7 @@ export class IncrementalCache implements IncrementalCacheType {
})
}

return this.cacheHandler?.revalidateTag?.(tag)
return this.cacheHandler?.revalidateTag?.(tags)
}

// x-ref: https://github.com/facebook/react/blob/2655c9354d8e1c54ba888444220f63e836925caa/packages/react/src/ReactFetch.js#L23
Expand Down
9 changes: 0 additions & 9 deletions packages/next/src/server/web/spec-extension/revalidate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,6 @@ function revalidate(tag: string, expression: string) {
store.revalidatedTags.push(tag)
}

if (!store.pendingRevalidates) {
store.pendingRevalidates = {}
}
store.pendingRevalidates[tag] = store.incrementalCache
.revalidateTag?.(tag)
.catch((err) => {
console.error(`revalidate failed for ${tag}`, err)
})

// TODO: only revalidate if the path matches
store.pathWasRevalidated = true
}

0 comments on commit 025f5b6

Please sign in to comment.