Skip to content

Commit

Permalink
Ensure we chunk revalidate tag requests (#70189)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
ijjk and kldavis4 committed Sep 17, 2024
1 parent 77910c8 commit 16ad368
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 24 deletions.
43 changes: 23 additions & 20 deletions packages/next/src/server/lib/incremental-cache/fetch-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,29 +176,32 @@ export default class FetchCache implements CacheHandler {
return
}

try {
const res = await fetchRetryWithTimeout(
`${this.cacheEndpoint}/v1/suspense-cache/revalidate?tags=${tags
.map((tag) => encodeURIComponent(tag))
.join(',')}`,
{
method: 'POST',
headers: this.headers,
// @ts-expect-error not on public type
next: { internal: true },
}
)
for (let i = 0; i < Math.ceil(tags.length / 64); i++) {
const currentTags = tags.slice(i * 64, i * 64 + 64)
try {
const res = await fetchRetryWithTimeout(
`${this.cacheEndpoint}/v1/suspense-cache/revalidate?tags=${currentTags
.map((tag) => encodeURIComponent(tag))
.join(',')}`,
{
method: 'POST',
headers: this.headers,
// @ts-expect-error not on public type
next: { internal: true },
}
)

if (res.status === 429) {
const retryAfter = res.headers.get('retry-after') || '60000'
rateLimitedUntil = Date.now() + parseInt(retryAfter)
}
if (res.status === 429) {
const retryAfter = res.headers.get('retry-after') || '60000'
rateLimitedUntil = Date.now() + parseInt(retryAfter)
}

if (!res.ok) {
throw new Error(`Request failed with status ${res.status}.`)
if (!res.ok) {
throw new Error(`Request failed with status ${res.status}.`)
}
} catch (err) {
console.warn(`Failed to revalidate tag`, currentTags, err)
}
} catch (err) {
console.warn(`Failed to revalidate tag ${tags}`, err)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'

export const dynamic = 'force-dynamic'

export function GET(req: NextRequest) {
for (let i = 0; i < 130; i++) {
revalidateTag(`thankyounext-${i}`)
}
return NextResponse.json({ done: true })
}
25 changes: 22 additions & 3 deletions test/production/app-dir/fetch-cache/fetch-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('fetch-cache', () => {
let nextInstance: any
let fetchGetReqIndex = 0
let revalidateReqIndex = 0
let revalidateReqShouldTimeout = false
let fetchGetShouldError = false
let fetchCacheServer: http.Server
let fetchCacheRequests: Array<{
Expand Down Expand Up @@ -101,6 +102,7 @@ describe('fetch-cache', () => {
fetchCacheRequests = []
storeCacheItems = false
fetchGetShouldError = false
revalidateReqShouldTimeout = false
fetchCacheServer = http.createServer(async (req, res) => {
console.log(`fetch cache request ${req.url} ${req.method}`, req.headers)
const parsedUrl = new URL(req.url || '/', 'http://n')
Expand All @@ -114,7 +116,8 @@ describe('fetch-cache', () => {
if (parsedUrl.pathname === '/v1/suspense-cache/revalidate') {
revalidateReqIndex += 1
// timeout unless it's 3rd retry
const shouldTimeout = revalidateReqIndex % 3 !== 0
const shouldTimeout =
revalidateReqShouldTimeout && revalidateReqIndex % 3 !== 0

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

it('should retry 3 times when revalidate times out', async () => {
await fetchViaHTTP(appPort, '/api/revalidate')
revalidateReqShouldTimeout = true
try {
await fetchViaHTTP(appPort, '/api/revalidate')

await retry(() => {
expect(revalidateReqIndex).toBe(3)
})
expect(cliOuptut).not.toContain('Failed to revalidate')
expect(cliOuptut).not.toContain('Error')
} finally {
revalidateReqShouldTimeout = false
}
})

it('should batch revalidate tag requests if > 64', async () => {
const revalidateReqIndexStart = revalidateReqIndex
await fetchViaHTTP(appPort, '/api/revalidate-alot')

await retry(() => {
expect(revalidateReqIndex).toBe(3)
expect(revalidateReqIndex).toBe(revalidateReqIndexStart + 3)
})
expect(cliOuptut).not.toContain('Failed to revalidate')
expect(cliOuptut).not.toContain('Error')
Expand Down
3 changes: 2 additions & 1 deletion test/turbopack-build-tests-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -14633,7 +14633,8 @@
"fetch-cache should have correct fetchUrl field for fetches and unstable_cache",
"fetch-cache should not retry for failed fetch-cache GET",
"fetch-cache should retry 3 times when revalidate times out",
"fetch-cache should update cache TTL even if cache data does not change"
"fetch-cache should update cache TTL even if cache data does not change",
"fetch-cache should batch revalidate tag requests if > 64"
],
"pending": [],
"flakey": [],
Expand Down

0 comments on commit 16ad368

Please sign in to comment.