Lower usage of KV cache to 30% of tags (#2500)

This commit is contained in:
Samy Pessé
2024-09-29 10:31:27 +02:00
committed by GitHub
parent b075f0f7e9
commit 56f5fa1fbe
4 changed files with 121 additions and 80 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@gitbook/cache-do': patch
---
Enable Workers observability with a sampling of 0.1
+104 -79
View File
@@ -53,16 +53,20 @@ export class CacheObject extends DurableObject {
* Get the value of a property. * Get the value of a property.
*/ */
public async get<Value = unknown>(key: string) { public async get<Value = unknown>(key: string) {
return timeFn(`get: ${key}`, async () => { return this.logOperation({ operation: 'get', key }, async (setLog) => {
// Try the memory state first. // Try the memory state first.
const memoryEntry = this.lru.get(key); const memoryEntry = this.lru.get(key);
if (memoryEntry) { if (memoryEntry) {
console.log(`get: (memory, ${!!memoryEntry.match}) ${key}`); setLog({ memory: true });
setLog({ memoryMatch: !!memoryEntry.match });
if (!memoryEntry.match) { if (!memoryEntry.match) {
return; return;
} }
if (memoryEntry.match.expiresAt > Date.now()) { const isExpired = memoryEntry.match.expiresAt < Date.now();
setLog({ memoryExpired: isExpired });
if (!isExpired) {
return memoryEntry.match.value as Value; return memoryEntry.match.value as Value;
} }
} }
@@ -75,28 +79,31 @@ export class CacheObject extends DurableObject {
* Get the value of a property from the DO storage. * Get the value of a property from the DO storage.
*/ */
public async getFromStorage<Value = unknown>(key: string) { public async getFromStorage<Value = unknown>(key: string) {
const entries = await this.ctx.storage.list<Uint8Array>({ return this.logOperation({ operation: 'getFromStorage', key }, async (setLog) => {
prefix: getStoragePropKey(key), const entries = await this.ctx.storage.list<Uint8Array>({
noCache: true, prefix: getStoragePropKey(key),
}); noCache: true,
if (entries.size) { });
const entry = decodeChunks<CacheObjectProp<Value>>(entries); if (entries.size) {
if (entry && entry.expiresAt > Date.now()) { const entry = decodeChunks<CacheObjectProp<Value>>(entries);
// Found setLog({ chunks: entries.size, chunksSize: entry?.size ?? 0 });
this.lru.set(key, { match: entry }); if (entry && entry.value.expiresAt > Date.now()) {
return entry.value; // Found
this.lru.set(key, { match: entry.value });
return entry.value.value;
}
} }
}
// Not found // Not found
this.lru.set(key, { match: undefined }); this.lru.set(key, { match: undefined });
});
} }
/** /**
* Set a value in the cache object. * Set a value in the cache object.
*/ */
public async set<Value = unknown>(key: string, value: Value, expiresAt: number) { public async set<Value = unknown>(key: string, value: Value, expiresAt: number) {
return timeFn(`set: ${key}`, async () => { return this.logOperation({ operation: 'set', key }, async (setLog) => {
const prop: CacheObjectProp<Value> = { const prop: CacheObjectProp<Value> = {
value, value,
expiresAt, expiresAt,
@@ -105,10 +112,12 @@ export class CacheObject extends DurableObject {
this.lru.set(key, { match: prop }); this.lru.set(key, { match: prop });
await this.ctx.storage.transaction(async (tx) => { await this.ctx.storage.transaction(async (tx) => {
const entries = encodeChunks(key, prop); const entries = encodeChunks(key, prop);
const chunks = Object.keys(entries).length;
setLog({ chunks });
const clockValue: CacheObjectExp = { const clockValue: CacheObjectExp = {
k: key, k: key,
c: Object.keys(entries).length, c: chunks,
}; };
await tx.put(getGCClockKey(key, expiresAt), clockValue); await tx.put(getGCClockKey(key, expiresAt), clockValue);
@@ -127,76 +136,101 @@ export class CacheObject extends DurableObject {
* Purge all keys in the cache object. * Purge all keys in the cache object.
*/ */
public async purge() { public async purge() {
let result = new Set<string>(); return this.logOperation({ operation: 'purge' }, async (setLog) => {
let result = new Set<string>();
try { try {
// List all the keys in the cache object. // List all the keys in the cache object.
const entries = await this.ctx.storage.list<CacheObjectExp>({ const entries = await this.ctx.storage.list<CacheObjectExp>({
prefix: 'exp.', prefix: 'exp.',
noCache: true, noCache: true,
}); });
console.log(`purge: ${entries.size} entries`); setLog({ entries: entries.size });
entries.forEach((exp) => { entries.forEach((exp) => {
result.add(exp.k); result.add(exp.k);
}); });
} catch (error) { } catch (error) {
// If an error occurs, reset the cache object. // If an error occurs, reset the cache object.
// This is a safety mechanism to prevent the cache object from being stuck in a bad state. // This is a safety mechanism to prevent the cache object from being stuck in a bad state.
console.error('Error during purge, resetting the cache object', error); console.error('Error during purge, resetting the cache object', error);
} }
await this.reset(); await this.reset();
return Array.from(result);
console.log(`purge returns`, Array.from(result)); });
return Array.from(result);
} }
/** /**
* Alarm to garbage collect all entries that have expired. * Alarm to garbage collect all entries that have expired.
*/ */
async alarm() { async alarm() {
try { return this.logOperation({ operation: 'alarm' }, async (setLog) => {
const entries = await this.ctx.storage.list<CacheObjectExp>({ try {
prefix: 'exp.', const entries = await this.ctx.storage.list<CacheObjectExp>({
noCache: true, prefix: 'exp.',
}); noCache: true,
const toDeleteSet = new Set<string>(); });
setLog({ entries: entries.size });
const toDeleteSet = new Set<string>();
for (const [key, exp] of entries) { for (const [key, exp] of entries) {
const timestamp = parseInt(key.split('.')[1]); const timestamp = parseInt(key.split('.')[1]);
if (timestamp < Date.now()) { if (timestamp < Date.now()) {
toDeleteSet.add(key); toDeleteSet.add(key);
for (let i = 0; i < exp.c; i++) { for (let i = 0; i < exp.c; i++) {
toDeleteSet.add(getStoragePropChunkKey(exp.k, i)); toDeleteSet.add(getStoragePropChunkKey(exp.k, i));
}
} }
} }
}
// Delete the keys by batch of 128. // Delete the keys by batch of 128.
const toDelete = Array.from(toDeleteSet); const toDelete = Array.from(toDeleteSet);
for (let i = 0; i < toDelete.length; i += 128) { setLog({ toDelete: toDelete.length });
await this.ctx.storage.delete(toDelete.slice(i, i + 128)); for (let i = 0; i < toDelete.length; i += 128) {
} await this.ctx.storage.delete(toDelete.slice(i, i + 128));
}
// If there are still keys to delete, set an alarm to continue the deletion in 12h. // If there are still keys to delete, set an alarm to continue the deletion in 12h.
if (toDelete.length) { if (toDelete.length) {
await this.ctx.storage.setAlarm(Date.now() + 12 * 60 * 60 * 1000); await this.ctx.storage.setAlarm(Date.now() + 12 * 60 * 60 * 1000);
}
} catch (error) {
// If an error occurs, reset the cache object.
// This is a safety mechanism to prevent the cache object from being stuck in a bad state.
console.error('Error during alarm, reset the cache object', error);
await this.reset();
} }
} catch (error) { });
// If an error occurs, reset the cache object.
// This is a safety mechanism to prevent the cache object from being stuck in a bad state.
console.error('Error during alarm, reset the cache object', error);
await this.reset();
}
} }
/** /**
* Reset the cache object. * Reset the cache object.
*/ */
async reset() { async reset() {
console.log('reset: clear all entries'); return this.logOperation({ operation: 'reset' }, async () => {
this.lru.clear(); this.lru.clear();
await this.ctx.storage.deleteAll(); await this.ctx.storage.deleteAll();
});
}
/**
* Time and log an operation.
*/
async logOperation<T>(
log: Record<string, unknown>,
fn: (update: (log: Record<string, unknown>) => void) => Promise<T>,
): Promise<T> {
const objectId = this.ctx.id.name ?? this.ctx.id.toString();
let update: Record<string, unknown> = {};
const start = performance.now();
try {
return await fn((arg) => {
Object.assign(update, arg);
});
} finally {
const duration = performance.now() - start;
console.log({ ...log, ...update, objectId, duration });
}
} }
} }
@@ -228,7 +262,7 @@ function encodeChunks<T>(key: string, value: T): Record<string, Uint8Array> {
return entries; return entries;
} }
function decodeChunks<T>(entries: Map<string, Uint8Array>): T | undefined { function decodeChunks<T>(entries: Map<string, Uint8Array>): { value: T; size: number } | undefined {
const chunks = Array.from(entries.entries()) const chunks = Array.from(entries.entries())
.map(([key, value]) => { .map(([key, value]) => {
const index = parseInt(key.split('.').pop()!); const index = parseInt(key.split('.').pop()!);
@@ -242,7 +276,7 @@ function decodeChunks<T>(entries: Map<string, Uint8Array>): T | undefined {
} }
const buf = mergeUint8Array(chunks); const buf = mergeUint8Array(chunks);
return decode(buf) as T; return { value: decode(buf) as T, size: buf.length };
} }
function chunkUint8Array(input: Uint8Array, chunkSize: number): Uint8Array[] { function chunkUint8Array(input: Uint8Array, chunkSize: number): Uint8Array[] {
@@ -263,12 +297,3 @@ function mergeUint8Array(chunks: Uint8Array[]): Uint8Array {
} }
return result; return result;
} }
async function timeFn<T>(message: string, fn: () => Promise<T>): Promise<T> {
const start = performance.now();
try {
return await fn();
} finally {
console.log(`${message} (${performance.now() - start}ms)`);
}
}
+11
View File
@@ -10,8 +10,19 @@ migrations = [
{tag = "v1", new_classes = ["CacheObject"]} {tag = "v1", new_classes = ["CacheObject"]}
] ]
[observability]
enabled = true
head_sampling_rate = 0.01
[env.preview] [env.preview]
name = "gitbook-open-cache-preview" name = "gitbook-open-cache-preview"
durable_objects.bindings = [ durable_objects.bindings = [
{name = "CACHE", class_name = "CacheObject"} {name = "CACHE", class_name = "CacheObject"}
] ]
migrations = [
{tag = "v1", new_classes = ["CacheObject"]}
]
[env.preview.observability]
enabled = true
head_sampling_rate = 1
+1 -1
View File
@@ -32,7 +32,7 @@ function shouldUseKVForTag(tag: string): boolean {
const hash = tag.split('').reduce((acc, char) => { const hash = tag.split('').reduce((acc, char) => {
return acc + char.charCodeAt(0); return acc + char.charCodeAt(0);
}, 0); }, 0);
if (hash % 100 <= 60) { if (hash % 100 <= 30) {
return true; return true;
} }