Skip to content

Commit 16ad368

Browse files
ijjkkldavis4
andcommitted
Ensure we chunk revalidate tag requests (#70189)
This ensures we chunk revalidate tag requests so we don't send too many at once. x-ref: [slack thread](https://vercel.slack.com/archives/C03UR7US95F/p1726585656900449?thread_ts=1726049244.204009&cid=C03UR7US95F) --------- Co-authored-by: Kelly Davis <kldavis4@users.noreply.github.com>
1 parent 77910c8 commit 16ad368

File tree

4 files changed

+58
-24
lines changed

4 files changed

+58
-24
lines changed

packages/next/src/server/lib/incremental-cache/fetch-cache.ts

+23-20
Original file line numberDiff line numberDiff line change
@@ -176,29 +176,32 @@ export default class FetchCache implements CacheHandler {
176176
return
177177
}
178178

179-
try {
180-
const res = await fetchRetryWithTimeout(
181-
`${this.cacheEndpoint}/v1/suspense-cache/revalidate?tags=${tags
182-
.map((tag) => encodeURIComponent(tag))
183-
.join(',')}`,
184-
{
185-
method: 'POST',
186-
headers: this.headers,
187-
// @ts-expect-error not on public type
188-
next: { internal: true },
189-
}
190-
)
179+
for (let i = 0; i < Math.ceil(tags.length / 64); i++) {
180+
const currentTags = tags.slice(i * 64, i * 64 + 64)
181+
try {
182+
const res = await fetchRetryWithTimeout(
183+
`${this.cacheEndpoint}/v1/suspense-cache/revalidate?tags=${currentTags
184+
.map((tag) => encodeURIComponent(tag))
185+
.join(',')}`,
186+
{
187+
method: 'POST',
188+
headers: this.headers,
189+
// @ts-expect-error not on public type
190+
next: { internal: true },
191+
}
192+
)
191193

192-
if (res.status === 429) {
193-
const retryAfter = res.headers.get('retry-after') || '60000'
194-
rateLimitedUntil = Date.now() + parseInt(retryAfter)
195-
}
194+
if (res.status === 429) {
195+
const retryAfter = res.headers.get('retry-after') || '60000'
196+
rateLimitedUntil = Date.now() + parseInt(retryAfter)
197+
}
196198

197-
if (!res.ok) {
198-
throw new Error(`Request failed with status ${res.status}.`)
199+
if (!res.ok) {
200+
throw new Error(`Request failed with status ${res.status}.`)
201+
}
202+
} catch (err) {
203+
console.warn(`Failed to revalidate tag`, currentTags, err)
199204
}
200-
} catch (err) {
201-
console.warn(`Failed to revalidate tag ${tags}`, err)
202205
}
203206
}
204207

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { revalidateTag } from 'next/cache'
2+
import { NextRequest, NextResponse } from 'next/server'
3+
4+
export const dynamic = 'force-dynamic'
5+
6+
export function GET(req: NextRequest) {
7+
for (let i = 0; i < 130; i++) {
8+
revalidateTag(`thankyounext-${i}`)
9+
}
10+
return NextResponse.json({ done: true })
11+
}

test/production/app-dir/fetch-cache/fetch-cache.test.ts

+22-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ describe('fetch-cache', () => {
1818
let nextInstance: any
1919
let fetchGetReqIndex = 0
2020
let revalidateReqIndex = 0
21+
let revalidateReqShouldTimeout = false
2122
let fetchGetShouldError = false
2223
let fetchCacheServer: http.Server
2324
let fetchCacheRequests: Array<{
@@ -101,6 +102,7 @@ describe('fetch-cache', () => {
101102
fetchCacheRequests = []
102103
storeCacheItems = false
103104
fetchGetShouldError = false
105+
revalidateReqShouldTimeout = false
104106
fetchCacheServer = http.createServer(async (req, res) => {
105107
console.log(`fetch cache request ${req.url} ${req.method}`, req.headers)
106108
const parsedUrl = new URL(req.url || '/', 'http://n')
@@ -114,7 +116,8 @@ describe('fetch-cache', () => {
114116
if (parsedUrl.pathname === '/v1/suspense-cache/revalidate') {
115117
revalidateReqIndex += 1
116118
// timeout unless it's 3rd retry
117-
const shouldTimeout = revalidateReqIndex % 3 !== 0
119+
const shouldTimeout =
120+
revalidateReqShouldTimeout && revalidateReqIndex % 3 !== 0
118121

119122
if (shouldTimeout) {
120123
console.log('not responding for', req.url, { revalidateReqIndex })
@@ -220,10 +223,26 @@ describe('fetch-cache', () => {
220223
})
221224

222225
it('should retry 3 times when revalidate times out', async () => {
223-
await fetchViaHTTP(appPort, '/api/revalidate')
226+
revalidateReqShouldTimeout = true
227+
try {
228+
await fetchViaHTTP(appPort, '/api/revalidate')
229+
230+
await retry(() => {
231+
expect(revalidateReqIndex).toBe(3)
232+
})
233+
expect(cliOuptut).not.toContain('Failed to revalidate')
234+
expect(cliOuptut).not.toContain('Error')
235+
} finally {
236+
revalidateReqShouldTimeout = false
237+
}
238+
})
239+
240+
it('should batch revalidate tag requests if > 64', async () => {
241+
const revalidateReqIndexStart = revalidateReqIndex
242+
await fetchViaHTTP(appPort, '/api/revalidate-alot')
224243

225244
await retry(() => {
226-
expect(revalidateReqIndex).toBe(3)
245+
expect(revalidateReqIndex).toBe(revalidateReqIndexStart + 3)
227246
})
228247
expect(cliOuptut).not.toContain('Failed to revalidate')
229248
expect(cliOuptut).not.toContain('Error')

test/turbopack-build-tests-manifest.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -14633,7 +14633,8 @@
1463314633
"fetch-cache should have correct fetchUrl field for fetches and unstable_cache",
1463414634
"fetch-cache should not retry for failed fetch-cache GET",
1463514635
"fetch-cache should retry 3 times when revalidate times out",
14636-
"fetch-cache should update cache TTL even if cache data does not change"
14636+
"fetch-cache should update cache TTL even if cache data does not change",
14637+
"fetch-cache should batch revalidate tag requests if > 64"
1463714638
],
1463814639
"pending": [],
1463914640
"flakey": [],

0 commit comments

Comments
 (0)