-
Notifications
You must be signed in to change notification settings - Fork 92
refactor: move tags handling from cache-handler module to dedicated tags-handler to allow for reuse #2872
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor: move tags handling from cache-handler module to dedicated tags-handler to allow for reuse #2872
Changes from 1 commit
a16c162
b9bec2e
6dece23
46e5e5b
9fb72e1
4ce9230
b6995a5
35a389c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { purgeCache } from '@netlify/functions' | ||
|
||
import { name as nextRuntimePkgName, version as nextRuntimePkgVersion } from '../../../package.json' | ||
import { TagManifest } from '../../shared/blob-types.cjs' | ||
import { | ||
getMemoizedKeyValueStoreBackedByRegionalBlobStore, | ||
MemoizedKeyValueStoreBackedByRegionalBlobStore, | ||
} from '../storage/storage.cjs' | ||
|
||
import { getLogger, getRequestContext } from './request-context.cjs' | ||
|
||
const purgeCacheUserAgent = `${nextRuntimePkgName}@${nextRuntimePkgVersion}` | ||
|
||
/** | ||
* Get timestamp of the last revalidation for a tag | ||
*/ | ||
async function lastTagRevalidationTimestamp( | ||
mrstork marked this conversation as resolved.
Show resolved
Hide resolved
|
||
tag: string, | ||
cacheStore: MemoizedKeyValueStoreBackedByRegionalBlobStore, | ||
): Promise<number | null> { | ||
const tagManifest = await cacheStore.get<TagManifest>(tag, 'tagManifest.get') | ||
if (!tagManifest) { | ||
return null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we wanted, we could return a number here as well and simplify the return type of this function to just There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pros & cons returning a number may simplify types and possibly remove the need to do specific Next.js |
||
} | ||
return tagManifest.revalidatedAt | ||
mrstork marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
/** | ||
* Check if any of the tags were invalidated since the given timestamp | ||
*/ | ||
export function isAnyTagStale(tags: string[], timestamp: number): Promise<boolean> { | ||
if (tags.length === 0 || !timestamp) { | ||
return Promise.resolve(false) | ||
} | ||
|
||
const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' }) | ||
|
||
// Full-route cache and fetch caches share a lot of tags | ||
// but we will only do actual blob read once withing a single request due to cacheStore | ||
// memoization. | ||
// Additionally, we will resolve the promise as soon as we find first | ||
// stale tag, so that we don't wait for all of them to resolve (but keep all | ||
// running in case future `CacheHandler.get` calls would be able to use results). | ||
// "Worst case" scenario is none of tag was invalidated in which case we need to wait | ||
// for all blob store checks to finish before we can be certain that no tag is stale. | ||
return new Promise<boolean>((resolve, reject) => { | ||
const tagManifestPromises: Promise<boolean>[] = [] | ||
|
||
for (const tag of tags) { | ||
const lastRevalidationTimestampPromise = lastTagRevalidationTimestamp(tag, cacheStore) | ||
|
||
tagManifestPromises.push( | ||
lastRevalidationTimestampPromise.then((lastRevalidationTimestamp) => { | ||
if (!lastRevalidationTimestamp) { | ||
// tag was never revalidated | ||
return false | ||
} | ||
const isStale = lastRevalidationTimestamp >= timestamp | ||
if (isStale) { | ||
// resolve outer promise immediately if any of the tags is stale | ||
resolve(true) | ||
return true | ||
} | ||
return false | ||
}), | ||
) | ||
} | ||
|
||
// make sure we resolve promise after all blobs are checked (if we didn't resolve as stale yet) | ||
Promise.all(tagManifestPromises) | ||
.then((tagManifestAreStale) => { | ||
resolve(tagManifestAreStale.some((tagIsStale) => tagIsStale)) | ||
}) | ||
.catch(reject) | ||
}) | ||
} | ||
|
||
/** | ||
* Transform a tag or tags into an array of tags and handle white space splitting and encoding | ||
*/ | ||
function getCacheTagsFromTagOrTags(tagOrTags: string | string[]): string[] { | ||
return (Array.isArray(tagOrTags) ? tagOrTags : [tagOrTags]) | ||
.flatMap((tag) => tag.split(/,|%2c/gi)) | ||
.filter(Boolean) | ||
} | ||
|
||
export function purgeEdgeCache(tagOrTags: string | string[]): void { | ||
const tags = getCacheTagsFromTagOrTags(tagOrTags) | ||
|
||
if (tags.length === 0) { | ||
return | ||
} | ||
|
||
const purgeCachePromise = purgeCache({ tags, userAgent: purgeCacheUserAgent }).catch((error) => { | ||
// TODO: add reporting here | ||
getLogger() | ||
.withError(error) | ||
.error(`[NetlifyCacheHandler]: Purging the cache for tags [${tags.join(',')}] failed`) | ||
}) | ||
|
||
getRequestContext()?.trackBackgroundWork(purgeCachePromise) | ||
} | ||
|
||
async function doRevalidateTag(tags: string[]): Promise<void> { | ||
getLogger().withFields({ tags }).debug('NetlifyCacheHandler.revalidateTag') | ||
|
||
if (tags.length === 0) { | ||
return | ||
} | ||
|
||
const data: TagManifest = { | ||
mrstork marked this conversation as resolved.
Show resolved
Hide resolved
|
||
revalidatedAt: Date.now(), | ||
} | ||
|
||
const cacheStore = getMemoizedKeyValueStoreBackedByRegionalBlobStore({ consistency: 'strong' }) | ||
|
||
await Promise.all( | ||
tags.map(async (tag) => { | ||
try { | ||
await cacheStore.set(tag, data, 'tagManifest.set') | ||
} catch (error) { | ||
getLogger().withError(error).log(`Failed to update tag manifest for ${tag}`) | ||
} | ||
}), | ||
) | ||
|
||
await purgeCache({ tags, userAgent: purgeCacheUserAgent }).catch((error) => { | ||
// TODO: add reporting here | ||
getLogger() | ||
.withError(error) | ||
.error(`[NetlifyCacheHandler]: Purging the cache for tags ${tags.join(', ')} failed`) | ||
}) | ||
} | ||
|
||
export function markTagsAsStaleAndPurgeEdgeCache(tagOrTags: string | string[]) { | ||
const tags = getCacheTagsFromTagOrTags(tagOrTags) | ||
|
||
const revalidateTagPromise = doRevalidateTag(tags) | ||
|
||
const requestContext = getRequestContext() | ||
if (requestContext) { | ||
requestContext.trackBackgroundWork(revalidateTagPromise) | ||
} | ||
|
||
return revalidateTagPromise | ||
} |
Uh oh!
There was an error while loading. Please reload this page.