From 5f7b29bbc8b2acd8e13240befe372f019a60ec95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samy=20Pess=C3=A9?= Date: Tue, 23 Jan 2024 18:21:46 +0100 Subject: [PATCH] Improve purging of cache and return keys (#111) * Use input url instead of request url * Improve cache purging * Lint * Fix TS * Add method to delete from cloudflare * Clear on all backends * Fix * Add global cloudflare tag --- src/app/~gitbook/revalidate/route.ts | 3 ++- src/lib/cache/cloudflare.ts | 17 +++++++++++- src/lib/cache/memory.ts | 9 ++++++- src/lib/cache/redis.ts | 36 ++++++++++++++++++------- src/lib/cache/revalidateTags.ts | 40 +++++++++++++++++++++++++--- src/lib/cache/types.ts | 10 ++++++- src/middleware.ts | 5 ++-- 7 files changed, 100 insertions(+), 20 deletions(-) diff --git a/src/app/~gitbook/revalidate/route.ts b/src/app/~gitbook/revalidate/route.ts index 3417a19d3..3413bc7f4 100644 --- a/src/app/~gitbook/revalidate/route.ts +++ b/src/app/~gitbook/revalidate/route.ts @@ -25,9 +25,10 @@ export async function POST(req: NextRequest) { ); } - await revalidateTags(json.tags, !!json.purge); + const result = await revalidateTags(json.tags, !!json.purge); return NextResponse.json({ success: true, + keys: result.keys, }); } diff --git a/src/lib/cache/cloudflare.ts b/src/lib/cache/cloudflare.ts index 0f72c3edf..844684ee3 100644 --- a/src/lib/cache/cloudflare.ts +++ b/src/lib/cache/cloudflare.ts @@ -33,8 +33,22 @@ export const cloudflareCache: CacheBackend = { await cache.put(cacheKey, serializeEntry(entry)); } }, + async del(keys) { + const cache = getCache(); + if (cache) { + await Promise.all( + keys.map(async (key) => { + const cacheKey = await serializeKey(key); + await cache.delete(cacheKey); + }), + ); + } + }, async revalidateTags(tags) { - return []; + return { + keys: [], + metas: [], + }; }, }; @@ -64,6 +78,7 @@ function serializeEntry(entry: CacheEntry): WorkerResponse { const headers = new Headers(); headers.set('Content-Type', 'application/json'); headers.set('Cache-Control', `public, max-age=${(entry.meta.expiresAt - Date.now()) / 1000}`); + headers.set('Cache-Tag', ['gitbook-open', ...entry.meta.tags].join(',')); // @ts-ignore return new Response(JSON.stringify(entry), { diff --git a/src/lib/cache/memory.ts b/src/lib/cache/memory.ts index 1ae482337..942662900 100644 --- a/src/lib/cache/memory.ts +++ b/src/lib/cache/memory.ts @@ -23,6 +23,10 @@ export const memoryCache: CacheBackend = { const memoryCache = getMemoryCache(); memoryCache.set(key, entry); }, + async del(keys) { + const memoryCache = getMemoryCache(); + keys.forEach((key) => memoryCache.delete(key)); + }, async revalidateTags(tags) { const memoryCache = getMemoryCache(); @@ -32,7 +36,10 @@ export const memoryCache: CacheBackend = { } }); - return []; + return { + keys: [], + metas: [], + }; }, }; diff --git a/src/lib/cache/redis.ts b/src/lib/cache/redis.ts index ff9117981..7cc8a194e 100644 --- a/src/lib/cache/redis.ts +++ b/src/lib/cache/redis.ts @@ -63,10 +63,24 @@ export const redisCache: CacheBackend = { await multi.exec(); }, - async revalidateTags(tags) { + async del(keys) { const redis = getRedis(); if (!redis) { - return []; + return; + } + + const multi = redis.multi(); + keys.forEach((key) => { + multi.del(getRedisKey(key)); + }); + + await multi.exec(); + }, + + async revalidateTags(tags, purge) { + const redis = getRedis(); + if (!redis) { + return { keys: [], metas: [] }; } const keys = new Set( @@ -77,13 +91,15 @@ export const redisCache: CacheBackend = { let metas: Array = []; if (keys.size > 0) { - metas = ( - await redis.json.mget( - // Hard limit to avoid fetching a massive list of data - Array.from(keys).slice(0, 1000), - '$.meta', - ) - ).flat() as Array; + if (!purge) { + metas = ( + await redis.json.mget( + // Hard limit to avoid fetching a massive list of data + Array.from(keys).slice(0, 1000), + '$.meta', + ) + ).flat() as Array; + } // Finally, delete all keys keys.forEach((key) => { @@ -96,7 +112,7 @@ export const redisCache: CacheBackend = { }); await pipeline.exec(); - return metas.filter(filterOutNullable); + return { keys: Array.from(keys), metas: metas.filter(filterOutNullable) }; }, }; diff --git a/src/lib/cache/revalidateTags.ts b/src/lib/cache/revalidateTags.ts index f4e138a65..5ff671b6e 100644 --- a/src/lib/cache/revalidateTags.ts +++ b/src/lib/cache/revalidateTags.ts @@ -8,17 +8,32 @@ import { CacheEntryMeta } from './types'; * Revalidate all values associated with tags. * It clears the values from the caches, but also start a background task to revalidate them. */ -export async function revalidateTags(tags: string[], purge: boolean): Promise { +export async function revalidateTags( + tags: string[], + purge: boolean, +): Promise<{ + keys: string[]; +}> { if (tags.length === 0) { - return; + return { keys: [] }; } const processed = new Set(); + + const keysByBackend = new Map(); + const keys = new Set(); const metas: CacheEntryMeta[] = []; await Promise.all( - cacheBackends.map(async (backend) => { - const addedMetas = await backend.revalidateTags(tags); + cacheBackends.map(async (backend, backendIndex) => { + const { keys: addedKeys, metas: addedMetas } = await backend.revalidateTags( + tags, + purge, + ); + addedKeys.forEach((key) => { + keys.add(key); + keysByBackend.set(backendIndex, [...(keysByBackend.get(backendIndex) ?? []), key]); + }); addedMetas.forEach((meta) => { const key = getCacheKey(meta.cache, meta.args); if (!processed.has(key)) { @@ -29,6 +44,19 @@ export async function revalidateTags(tags: string[], purge: boolean): Promise { + const unclearedKeys = Array.from(keys).filter( + (key) => !keysByBackend.get(backendIndex)?.includes(key), + ); + + if (unclearedKeys.length > 0) { + await backend.del(unclearedKeys); + } + }), + ); + // Refresh the values in the cache if (metas && !purge) { await pMap( @@ -46,4 +74,8 @@ export async function revalidateTags(tags: string[], purge: boolean): Promise; + /** + * Delete a value from the cache. + */ + del(keys: string[]): Promise; + /** * Revalidate all keys associated with tags. * It should return the meta of all entries that were revalidated. */ - revalidateTags(tags: string[]): Promise; + revalidateTags( + tags: string[], + purge: boolean, + ): Promise<{ keys: string[]; metas: CacheEntryMeta[] }>; } diff --git a/src/middleware.ts b/src/middleware.ts index d59395773..16668687f 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -65,10 +65,11 @@ export type LookupResult = PublishedContentWithCache & { export async function middleware(request: NextRequest) { const { url, mode } = getInputURL(request); - Sentry.setTag('url', request.url); + Sentry.setTag('url', url.toString()); Sentry.setContext('request', { method: request.method, - url: request.url, + url: url.toString(), + rawRequestURL: request.url, userAgent: userAgent(), });