From ef065715c79ae273f94578d0bd5fd5379846dbb3 Mon Sep 17 00:00:00 2001 From: JJ Kasper Date: Tue, 17 Sep 2024 09:09:01 -0700 Subject: [PATCH] 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 --- .../lib/incremental-cache/fetch-cache.ts | 43 ++++++++++--------- .../app/api/revalidate-alot/route.ts | 11 +++++ .../app-dir/fetch-cache/fetch-cache.test.ts | 25 +++++++++-- test/turbopack-build-tests-manifest.json | 3 +- 4 files changed, 58 insertions(+), 24 deletions(-) create mode 100644 test/production/app-dir/fetch-cache/app/api/revalidate-alot/route.ts diff --git a/packages/next/src/server/lib/incremental-cache/fetch-cache.ts b/packages/next/src/server/lib/incremental-cache/fetch-cache.ts index 599ad0c4707bb..b58a7ed98a8c9 100644 --- a/packages/next/src/server/lib/incremental-cache/fetch-cache.ts +++ b/packages/next/src/server/lib/incremental-cache/fetch-cache.ts @@ -182,29 +182,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) } } diff --git a/test/production/app-dir/fetch-cache/app/api/revalidate-alot/route.ts b/test/production/app-dir/fetch-cache/app/api/revalidate-alot/route.ts new file mode 100644 index 0000000000000..a418da3eff3cb --- /dev/null +++ b/test/production/app-dir/fetch-cache/app/api/revalidate-alot/route.ts @@ -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 }) +} diff --git a/test/production/app-dir/fetch-cache/fetch-cache.test.ts b/test/production/app-dir/fetch-cache/fetch-cache.test.ts index 6290bab790d75..1838ae47aabf7 100644 --- a/test/production/app-dir/fetch-cache/fetch-cache.test.ts +++ b/test/production/app-dir/fetch-cache/fetch-cache.test.ts @@ -19,6 +19,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<{ @@ -112,6 +113,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') @@ -125,7 +127,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 }) @@ -231,10 +234,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') diff --git a/test/turbopack-build-tests-manifest.json b/test/turbopack-build-tests-manifest.json index 34122f616a179..02acfdc60e527 100644 --- a/test/turbopack-build-tests-manifest.json +++ b/test/turbopack-build-tests-manifest.json @@ -15801,7 +15801,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": [],