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
This commit is contained in:
Samy Pessé
2024-01-23 18:21:46 +01:00
committed by GitHub
parent 756b5a766f
commit 5f7b29bbc8
7 changed files with 100 additions and 20 deletions
+2 -1
View File
@@ -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,
});
}
+16 -1
View File
@@ -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), {
+8 -1
View File
@@ -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: [],
};
},
};
+26 -10
View File
@@ -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<CacheEntryMeta | null> = [];
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<CacheEntryMeta | null>;
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<CacheEntryMeta | null>;
}
// 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) };
},
};
+36 -4
View File
@@ -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<void> {
export async function revalidateTags(
tags: string[],
purge: boolean,
): Promise<{
keys: string[];
}> {
if (tags.length === 0) {
return;
return { keys: [] };
}
const processed = new Set<string>();
const keysByBackend = new Map<number, string[]>();
const keys = new Set<string>();
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<vo
}),
);
// Clear the keys on the backends that didn't return them
await Promise.all(
cacheBackends.map(async (backend, backendIndex) => {
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<vo
},
);
}
return {
keys: Array.from(keys.keys()),
};
}
+9 -1
View File
@@ -48,9 +48,17 @@ export interface CacheBackend {
*/
set(key: string, entry: CacheEntry): Promise<void>;
/**
* Delete a value from the cache.
*/
del(keys: string[]): Promise<void>;
/**
* Revalidate all keys associated with tags.
* It should return the meta of all entries that were revalidated.
*/
revalidateTags(tags: string[]): Promise<CacheEntryMeta[]>;
revalidateTags(
tags: string[],
purge: boolean,
): Promise<{ keys: string[]; metas: CacheEntryMeta[] }>;
}
+3 -2
View File
@@ -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(),
});