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:
Samy Pessé
2024-03-08 12:05:32 +00:00
committed by GitHub
parent 5a5b4bb93c
commit f9e3835c2f
8 changed files with 54 additions and 52 deletions
-1
View File
@@ -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,
},
};
+2 -2
View File
@@ -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) {
+2 -2
View File
@@ -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;
});
-1
View File
@@ -16,7 +16,6 @@ export const memoryCache: CacheBackend = {
}
if (memoryEntry.meta.expiresAt > Date.now()) {
memoryEntry.meta.hits + 1;
return memoryEntry;
} else {
memoryCache.delete(key);
+43 -30
View File
@@ -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.
*/
+2 -6
View File
@@ -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);
-5
View File
@@ -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
View File
@@ -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();