mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-24 11:26:31 +00:00
Simplify redis cache and remove hits count (#225)
* Simplify redis cache and remove hits count * Fix naming * Update src/lib/tracing.ts Co-authored-by: Steven H <shne24@gmail.com> --------- Co-authored-by: Steven H <shne24@gmail.com>
This commit is contained in:
Vendored
-1
@@ -76,7 +76,6 @@ export function cache<Args extends any[], Result>(
|
||||
expiresAt:
|
||||
Date.now() + (result.ttl ?? options.defaultTtl ?? 60 * 60 * 24) * 1000,
|
||||
args,
|
||||
hits: 1,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -21,10 +21,10 @@ export const cloudflareCache: CacheBackend = {
|
||||
if (!cache) {
|
||||
return null;
|
||||
}
|
||||
return trace(`cloudflareCache.get(${key})`, async (trace) => {
|
||||
return trace(`cloudflareCache.get(${key})`, async (span) => {
|
||||
const cacheKey = await serializeKey(key);
|
||||
const response = await cache.match(cacheKey);
|
||||
trace.setAttribute('hit', !!response);
|
||||
span.setAttribute('hit', !!response);
|
||||
|
||||
options?.signal?.throwIfAborted();
|
||||
if (!response) {
|
||||
|
||||
Vendored
+2
-2
@@ -24,7 +24,7 @@ export const cloudflareKVCache: CacheBackend = {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trace(`cloudflareKV.get(${key})`, async (trace) => {
|
||||
return trace(`cloudflareKV.get(${key})`, async (span) => {
|
||||
const kvKey = getValueKey(key);
|
||||
|
||||
const entry = await kv.get<CacheEntry>(kvKey, {
|
||||
@@ -32,7 +32,7 @@ export const cloudflareKVCache: CacheBackend = {
|
||||
cacheTtl: 2 * 60,
|
||||
});
|
||||
|
||||
trace.setAttribute('hit', !!entry);
|
||||
span.setAttribute('hit', !!entry);
|
||||
|
||||
return entry;
|
||||
});
|
||||
|
||||
Vendored
-1
@@ -16,7 +16,6 @@ export const memoryCache: CacheBackend = {
|
||||
}
|
||||
|
||||
if (memoryEntry.meta.expiresAt > Date.now()) {
|
||||
memoryEntry.meta.hits + 1;
|
||||
return memoryEntry;
|
||||
} else {
|
||||
memoryCache.delete(key);
|
||||
|
||||
Vendored
+43
-30
@@ -1,13 +1,12 @@
|
||||
import { Redis } from '@upstash/redis/cloudflare';
|
||||
|
||||
import { CacheBackend, CacheEntry, CacheEntryMeta } from './types';
|
||||
import { CacheBackend, CacheEntryMeta } from './types';
|
||||
import { getCacheMaxAge } from './utils';
|
||||
import { trace } from '../tracing';
|
||||
import { filterOutNullable } from '../typescript';
|
||||
import { waitUntil } from '../waitUntil';
|
||||
|
||||
const cacheNamespace = process.env.UPSTASH_REDIS_NAMESPACE ?? 'gitbook';
|
||||
const cacheVersion = 1;
|
||||
const cacheVersion = 2;
|
||||
|
||||
export const redisCache: CacheBackend = {
|
||||
name: 'redis',
|
||||
@@ -17,23 +16,17 @@ export const redisCache: CacheBackend = {
|
||||
if (!redis) {
|
||||
return null;
|
||||
}
|
||||
return trace(`redis.get(${key})`, async (trace) => {
|
||||
const redisKey = getRedisKey(key);
|
||||
const redisEntry = await redis.json.get(redisKey);
|
||||
return trace(`redis.get(${key})`, async (span) => {
|
||||
const valueKey = getCacheEntryKey(key, 'value');
|
||||
const redisEntry = await redis.get<string>(valueKey);
|
||||
|
||||
trace.setAttribute('hit', !!redisEntry);
|
||||
span.setAttribute('hit', !!redisEntry);
|
||||
|
||||
if (!redisEntry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await waitUntil(
|
||||
redis.json.numincrby(redisKey, '$.meta.hits', 1).catch((error) => {
|
||||
// Ignore errors
|
||||
}),
|
||||
);
|
||||
|
||||
return redisEntry as CacheEntry;
|
||||
return JSON.parse(redisEntry);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -43,30 +36,32 @@ export const redisCache: CacheBackend = {
|
||||
return;
|
||||
}
|
||||
|
||||
return trace(`redis.set(${key})`, async (trace) => {
|
||||
const multi = redis.multi();
|
||||
|
||||
const redisKey = getRedisKey(key);
|
||||
return trace(`redis.set(${key})`, async () => {
|
||||
const expire = getCacheMaxAge(entry.meta);
|
||||
|
||||
// Don't cache for less than 1min, as it's not worth it
|
||||
// Don't cache for less than 10min, as it's not worth it
|
||||
if (expire <= 10 * 60) {
|
||||
return;
|
||||
}
|
||||
|
||||
const multi = redis.multi();
|
||||
const valueKey = getCacheEntryKey(key, 'value');
|
||||
const metaKey = getCacheEntryKey(key, 'meta');
|
||||
|
||||
entry.meta.tags.forEach((tag) => {
|
||||
const redisTagKey = getCacheTagKey(tag);
|
||||
|
||||
multi.sadd(redisTagKey, redisKey);
|
||||
multi.sadd(redisTagKey, key);
|
||||
|
||||
// Set am expiration on the tag to be the maximum of the expiration of all keys
|
||||
multi.expire(redisTagKey, expire, 'GT');
|
||||
multi.expire(redisTagKey, expire, 'NX');
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
multi.json.set(redisKey, '$', entry);
|
||||
multi.expire(redisKey, expire);
|
||||
multi.set(valueKey, JSON.stringify(entry));
|
||||
multi.set(metaKey, JSON.stringify(entry.meta));
|
||||
multi.expire(valueKey, expire);
|
||||
multi.expire(metaKey, expire);
|
||||
|
||||
await multi.exec();
|
||||
});
|
||||
@@ -80,7 +75,8 @@ export const redisCache: CacheBackend = {
|
||||
|
||||
const multi = redis.multi();
|
||||
keys.forEach((key) => {
|
||||
multi.del(getRedisKey(key));
|
||||
multi.del(getCacheEntryKey(key, 'value'));
|
||||
multi.del(getCacheEntryKey(key, 'meta'));
|
||||
});
|
||||
|
||||
await multi.exec();
|
||||
@@ -100,25 +96,35 @@ export const redisCache: CacheBackend = {
|
||||
let metas: Array<CacheEntryMeta | null> = [];
|
||||
|
||||
if (keys.size > 0) {
|
||||
// Read the meta
|
||||
if (!purge) {
|
||||
metas = (
|
||||
await redis.json.mget(
|
||||
await redis.mget<Array<string | null>>(
|
||||
// Hard limit to avoid fetching a massive list of data
|
||||
// Starts with the smallest keys.
|
||||
Array.from(keys)
|
||||
.sort((a, b) => a.length - b.length)
|
||||
.slice(0, 10),
|
||||
'$.meta',
|
||||
.slice(0, 50)
|
||||
.map((key) => getCacheEntryKey(key, 'meta')),
|
||||
)
|
||||
).flat() as Array<CacheEntryMeta | null>;
|
||||
)
|
||||
.flat()
|
||||
.map((rawMeta) => {
|
||||
if (!rawMeta) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(rawMeta);
|
||||
})
|
||||
.filter(filterOutNullable);
|
||||
}
|
||||
|
||||
// Finally, delete all keys
|
||||
// Delete all keys
|
||||
keys.forEach((key) => {
|
||||
pipeline.del(key);
|
||||
pipeline.del(getCacheEntryKey(key, 'value'));
|
||||
});
|
||||
}
|
||||
|
||||
// And delete the tags
|
||||
tags.forEach((tag) => {
|
||||
pipeline.del(getCacheTagKey(tag));
|
||||
});
|
||||
@@ -148,6 +154,13 @@ function getCacheTagKey(tag: string) {
|
||||
return getRedisKey(`tags.${tag}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key for an entry.
|
||||
*/
|
||||
function getCacheEntryKey(key: string, type: 'meta' | 'value') {
|
||||
return getRedisKey(`entry.${key}.${type}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a redis key for a cache entry.
|
||||
*/
|
||||
|
||||
Vendored
+2
-6
@@ -62,13 +62,9 @@ export async function revalidateTags(
|
||||
if (metas && !purge) {
|
||||
await waitUntil(
|
||||
pMap(
|
||||
// Sort to process the entries with the most hits first
|
||||
metas.sort((a, b) => b.hits - a.hits),
|
||||
metas,
|
||||
async (meta) => {
|
||||
console.log(
|
||||
`revalidating ${meta.cache} (${meta.hits} hits) with args`,
|
||||
meta.args,
|
||||
);
|
||||
console.log(`revalidating ${meta.cache} with args`, meta.args);
|
||||
const cache = getCache(meta.cache);
|
||||
if (cache) {
|
||||
await cache.revalidate(...meta.args);
|
||||
|
||||
Vendored
-5
@@ -18,11 +18,6 @@ export interface CacheEntryMeta {
|
||||
* Arguments that were passed to the function.
|
||||
*/
|
||||
args: any[];
|
||||
|
||||
/**
|
||||
* Number of hits on this entry.
|
||||
*/
|
||||
hits: number;
|
||||
}
|
||||
|
||||
export interface CacheEntry {
|
||||
|
||||
+5
-5
@@ -1,13 +1,13 @@
|
||||
export interface Trace {
|
||||
export interface TraceSpan {
|
||||
setAttribute: (label: string, value: boolean | string | number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a performance trace for the given function.
|
||||
*/
|
||||
export async function trace<T>(name: string, fn: (trace: Trace) => Promise<T>): Promise<T> {
|
||||
export async function trace<T>(name: string, fn: (span: TraceSpan) => Promise<T>): Promise<T> {
|
||||
const attributes: Record<string, boolean | string | number> = {};
|
||||
const trace: Trace = {
|
||||
const span: TraceSpan = {
|
||||
setAttribute(label, value) {
|
||||
attributes[label] = value;
|
||||
},
|
||||
@@ -15,9 +15,9 @@ export async function trace<T>(name: string, fn: (trace: Trace) => Promise<T>):
|
||||
|
||||
let start = now();
|
||||
try {
|
||||
return await fn(trace);
|
||||
return await fn(span);
|
||||
} catch (error) {
|
||||
trace.setAttribute('error', true);
|
||||
span.setAttribute('error', true);
|
||||
throw error;
|
||||
} finally {
|
||||
let end = now();
|
||||
|
||||
Reference in New Issue
Block a user