mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-18 00:25:07 +00:00
Lower usage of KV cache to 30% of tags (#2500)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@gitbook/cache-do': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Enable Workers observability with a sampling of 0.1
|
||||||
@@ -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)`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user