Skip to content

Commit cd26d2c

Browse files
authored
Merge branch 'main' into serhalp/frb-1747-remove-srcrunhandlerstracingts-module-from-next-runtime
2 parents 733a479 + e02356e commit cd26d2c

File tree

2 files changed

+148
-103
lines changed

2 files changed

+148
-103
lines changed

src/run/handlers/cache.cts

+7-103
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@ import { Buffer } from 'node:buffer'
55
import { join } from 'node:path'
66
import { join as posixJoin } from 'node:path/posix'
77

8-
import { purgeCache } from '@netlify/functions'
98
import { type Span } from '@opentelemetry/api'
109
import type { PrerenderManifest } from 'next/dist/build/index.js'
1110
import { NEXT_CACHE_TAGS_HEADER } from 'next/dist/lib/constants.js'
1211

13-
import { name as nextRuntimePkgName, version as nextRuntimePkgVersion } from '../../../package.json'
14-
import { type TagManifest } from '../../shared/blob-types.cjs'
1512
import {
1613
type CacheHandlerContext,
1714
type CacheHandlerForMultipleVersions,
@@ -28,10 +25,9 @@ import {
2825
} from '../storage/storage.cjs'
2926

3027
import { getLogger, getRequestContext } from './request-context.cjs'
28+
import { isAnyTagStale, markTagsAsStaleAndPurgeEdgeCache, purgeEdgeCache } from './tags-handler.cjs'
3129
import { getTracer, recordWarning } from './tracer.cjs'
3230

33-
const purgeCacheUserAgent = `${nextRuntimePkgName}@${nextRuntimePkgVersion}`
34-
3531
export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
3632
options: CacheHandlerContext
3733
revalidatedTags: string[]
@@ -427,70 +423,15 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
427423
if (requestContext?.didPagesRouterOnDemandRevalidate) {
428424
// encode here to deal with non ASCII characters in the key
429425
const tag = `_N_T_${key === '/index' ? '/' : encodeURI(key)}`
430-
const tags = tag.split(/,|%2c/gi).filter(Boolean)
431-
432-
if (tags.length === 0) {
433-
return
434-
}
435426

436-
getLogger().debug(`Purging CDN cache for: [${tag}]`)
437-
requestContext.trackBackgroundWork(
438-
purgeCache({ tags, userAgent: purgeCacheUserAgent }).catch((error) => {
439-
// TODO: add reporting here
440-
getLogger()
441-
.withError(error)
442-
.error(`[NetlifyCacheHandler]: Purging the cache for tag ${tag} failed`)
443-
}),
444-
)
427+
requestContext?.trackBackgroundWork(purgeEdgeCache(tag))
445428
}
446429
}
447430
})
448431
}
449432

450-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
451-
async revalidateTag(tagOrTags: string | string[], ...args: any) {
452-
const revalidateTagPromise = this.doRevalidateTag(tagOrTags, ...args)
453-
454-
const requestContext = getRequestContext()
455-
if (requestContext) {
456-
requestContext.trackBackgroundWork(revalidateTagPromise)
457-
}
458-
459-
return revalidateTagPromise
460-
}
461-
462-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
463-
private async doRevalidateTag(tagOrTags: string | string[], ...args: any) {
464-
getLogger().withFields({ tagOrTags, args }).debug('NetlifyCacheHandler.revalidateTag')
465-
466-
const tags = (Array.isArray(tagOrTags) ? tagOrTags : [tagOrTags])
467-
.flatMap((tag) => tag.split(/,|%2c/gi))
468-
.filter(Boolean)
469-
470-
if (tags.length === 0) {
471-
return
472-
}
473-
474-
const data: TagManifest = {
475-
revalidatedAt: Date.now(),
476-
}
477-
478-
await Promise.all(
479-
tags.map(async (tag) => {
480-
try {
481-
await this.cacheStore.set(tag, data, 'tagManifest.set')
482-
} catch (error) {
483-
getLogger().withError(error).log(`Failed to update tag manifest for ${tag}`)
484-
}
485-
}),
486-
)
487-
488-
await purgeCache({ tags, userAgent: purgeCacheUserAgent }).catch((error) => {
489-
// TODO: add reporting here
490-
getLogger()
491-
.withError(error)
492-
.error(`[NetlifyCacheHandler]: Purging the cache for tags ${tags.join(', ')} failed`)
493-
})
433+
async revalidateTag(tagOrTags: string | string[]) {
434+
return markTagsAsStaleAndPurgeEdgeCache(tagOrTags)
494435
}
495436

496437
resetRequestCache() {
@@ -501,7 +442,7 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
501442
/**
502443
* Checks if a cache entry is stale through on demand revalidated tags
503444
*/
504-
private async checkCacheEntryStaleByTags(
445+
private checkCacheEntryStaleByTags(
505446
cacheEntry: NetlifyCacheHandlerValue,
506447
tags: string[] = [],
507448
softTags: string[] = [],
@@ -534,45 +475,8 @@ export class NetlifyCacheHandler implements CacheHandlerForMultipleVersions {
534475
}
535476

536477
// 2. If any in-memory tags don't indicate that any of tags was invalidated
537-
// we will check blob store. Full-route cache and fetch caches share a lot of tags
538-
// but we will only do actual blob read once withing a single request due to cacheStore
539-
// memoization.
540-
// Additionally, we will resolve the promise as soon as we find first
541-
// stale tag, so that we don't wait for all of them to resolve (but keep all
542-
// running in case future `CacheHandler.get` calls would be able to use results).
543-
// "Worst case" scenario is none of tag was invalidated in which case we need to wait
544-
// for all blob store checks to finish before we can be certain that no tag is stale.
545-
return new Promise<boolean>((resolve, reject) => {
546-
const tagManifestPromises: Promise<boolean>[] = []
547-
548-
for (const tag of cacheTags) {
549-
const tagManifestPromise: Promise<TagManifest | null> = this.cacheStore.get<TagManifest>(
550-
tag,
551-
'tagManifest.get',
552-
)
553-
554-
tagManifestPromises.push(
555-
tagManifestPromise.then((tagManifest) => {
556-
if (!tagManifest) {
557-
return false
558-
}
559-
const isStale = tagManifest.revalidatedAt >= (cacheEntry.lastModified || Date.now())
560-
if (isStale) {
561-
resolve(true)
562-
return true
563-
}
564-
return false
565-
}),
566-
)
567-
}
568-
569-
// make sure we resolve promise after all blobs are checked (if we didn't resolve as stale yet)
570-
Promise.all(tagManifestPromises)
571-
.then((tagManifestAreStale) => {
572-
resolve(tagManifestAreStale.some((tagIsStale) => tagIsStale))
573-
})
574-
.catch(reject)
575-
})
478+
// we will check blob store.
479+
return isAnyTagStale(cacheTags, cacheEntry.lastModified)
576480
}
577481
}
578482

src/run/handlers/tags-handler.cts

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { purgeCache } from '@netlify/functions'
2+
3+
import { name as nextRuntimePkgName, version as nextRuntimePkgVersion } from '../../../package.json'
4+
import { TagManifest } from '../../shared/blob-types.cjs'
5+
import {
6+
getMemoizedKeyValueStoreBackedByRegionalBlobStore,
7+
MemoizedKeyValueStoreBackedByRegionalBlobStore,
8+
} from '../storage/storage.cjs'
9+
10+
import { getLogger, getRequestContext } from './request-context.cjs'
11+
12+
const purgeCacheUserAgent = `${nextRuntimePkgName}@${nextRuntimePkgVersion}`
13+
14+
/**
15+
* Get timestamp of the last revalidation for a tag
16+
*/
17+
async function getTagRevalidatedAt(
18+
tag: string,
19+
cacheStore: MemoizedKeyValueStoreBackedByRegionalBlobStore,
20+
): Promise<number | null> {
21+
const tagManifest = await cacheStore.get<TagManifest>(tag, 'tagManifest.get')
22+
if (!tagManifest) {
23+
return null
24+
}
25+
return tagManifest.revalidatedAt
26+
}
27+
28+
/**
29+
* Check if any of the tags were invalidated since the given timestamp
30+
*/
31+
export function isAnyTagStale(tags: string[], timestamp: number): Promise<boolean> {
32+
if (tags.length === 0 || !timestamp) {
33+
return Promise.resolve(false)
34+
}
35+
36+
const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' })
37+
38+
// Full-route cache and fetch caches share a lot of tags
39+
// but we will only do actual blob read once withing a single request due to cacheStore
40+
// memoization.
41+
// Additionally, we will resolve the promise as soon as we find first
42+
// stale tag, so that we don't wait for all of them to resolve (but keep all
43+
// running in case future `CacheHandler.get` calls would be able to use results).
44+
// "Worst case" scenario is none of tag was invalidated in which case we need to wait
45+
// for all blob store checks to finish before we can be certain that no tag is stale.
46+
return new Promise<boolean>((resolve, reject) => {
47+
const tagManifestPromises: Promise<boolean>[] = []
48+
49+
for (const tag of tags) {
50+
const lastRevalidationTimestampPromise = getTagRevalidatedAt(tag, cacheStore)
51+
52+
tagManifestPromises.push(
53+
lastRevalidationTimestampPromise.then((lastRevalidationTimestamp) => {
54+
if (!lastRevalidationTimestamp) {
55+
// tag was never revalidated
56+
return false
57+
}
58+
const isStale = lastRevalidationTimestamp >= timestamp
59+
if (isStale) {
60+
// resolve outer promise immediately if any of the tags is stale
61+
resolve(true)
62+
return true
63+
}
64+
return false
65+
}),
66+
)
67+
}
68+
69+
// make sure we resolve promise after all blobs are checked (if we didn't resolve as stale yet)
70+
Promise.all(tagManifestPromises)
71+
.then((tagManifestAreStale) => {
72+
resolve(tagManifestAreStale.some((tagIsStale) => tagIsStale))
73+
})
74+
.catch(reject)
75+
})
76+
}
77+
78+
/**
79+
* Transform a tag or tags into an array of tags and handle white space splitting and encoding
80+
*/
81+
function getCacheTagsFromTagOrTags(tagOrTags: string | string[]): string[] {
82+
return (Array.isArray(tagOrTags) ? tagOrTags : [tagOrTags])
83+
.flatMap((tag) => tag.split(/,|%2c/gi))
84+
.filter(Boolean)
85+
}
86+
87+
export function purgeEdgeCache(tagOrTags: string | string[]): Promise<void> {
88+
const tags = getCacheTagsFromTagOrTags(tagOrTags)
89+
90+
if (tags.length === 0) {
91+
return Promise.resolve()
92+
}
93+
94+
getLogger().debug(`[NextRuntime] Purging CDN cache for: [${tags}.join(', ')]`)
95+
96+
return purgeCache({ tags, userAgent: purgeCacheUserAgent }).catch((error) => {
97+
// TODO: add reporting here
98+
getLogger()
99+
.withError(error)
100+
.error(`[NextRuntime] Purging the cache for tags [${tags.join(',')}] failed`)
101+
})
102+
}
103+
104+
async function doRevalidateTagAndPurgeEdgeCache(tags: string[]): Promise<void> {
105+
getLogger().withFields({ tags }).debug('doRevalidateTagAndPurgeEdgeCache')
106+
107+
if (tags.length === 0) {
108+
return
109+
}
110+
111+
const tagManifest: TagManifest = {
112+
revalidatedAt: Date.now(),
113+
}
114+
115+
const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' })
116+
117+
await Promise.all(
118+
tags.map(async (tag) => {
119+
try {
120+
await cacheStore.set(tag, tagManifest, 'tagManifest.set')
121+
} catch (error) {
122+
getLogger().withError(error).log(`[NextRuntime] Failed to update tag manifest for ${tag}`)
123+
}
124+
}),
125+
)
126+
127+
await purgeEdgeCache(tags)
128+
}
129+
130+
export function markTagsAsStaleAndPurgeEdgeCache(tagOrTags: string | string[]) {
131+
const tags = getCacheTagsFromTagOrTags(tagOrTags)
132+
133+
const revalidateTagPromise = doRevalidateTagAndPurgeEdgeCache(tags)
134+
135+
const requestContext = getRequestContext()
136+
if (requestContext) {
137+
requestContext.trackBackgroundWork(revalidateTagPromise)
138+
}
139+
140+
return revalidateTagPromise
141+
}

0 commit comments

Comments
 (0)