From ecaf152b4a6eb702375debaef0dddc2cca798116 Mon Sep 17 00:00:00 2001 From: syuilo Date: Tue, 4 Apr 2023 17:32:09 +0900 Subject: enhance(backend): improve cache --- packages/backend/src/misc/cache.ts | 97 +++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 6 deletions(-) (limited to 'packages/backend/src/misc') diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index a805d18421..870dfd237c 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -1,9 +1,94 @@ +import Redis from 'ioredis'; import { bindThis } from '@/decorators.js'; +// redis通すとDateのインスタンスはstringに変換されるので +type Serialized = { + [K in keyof T]: + T[K] extends Date + ? string + : T[K] extends (Date | null) + ? (string | null) + : T[K] extends Record + ? Serialized + : T[K]; +}; + +export class RedisKVCache { + private redisClient: Redis.Redis; + private name: string; + private lifetime: number; + private memoryCache: MemoryKVCache; + + constructor(redisClient: RedisKVCache['redisClient'], name: RedisKVCache['name'], lifetime: RedisKVCache['lifetime'], memoryCacheLifetime: number) { + this.redisClient = redisClient; + this.name = name; + this.lifetime = lifetime; + this.memoryCache = new MemoryKVCache(memoryCacheLifetime); + } + + @bindThis + public async set(key: string, value: T): Promise { + this.memoryCache.set(key, value); + if (this.lifetime === Infinity) { + await this.redisClient.set( + `kvcache:${this.name}:${key}`, + JSON.stringify(value), + ); + } else { + await this.redisClient.set( + `kvcache:${this.name}:${key}`, + JSON.stringify(value), + 'ex', Math.round(this.lifetime / 1000), + ); + } + } + + @bindThis + public async get(key: string): Promise | T | undefined> { + const memoryCached = this.memoryCache.get(key); + if (memoryCached !== undefined) return memoryCached; + + const cached = await this.redisClient.get(`kvcache:${this.name}:${key}`); + if (cached == null) return undefined; + return JSON.parse(cached); + } + + @bindThis + public async delete(key: string): Promise { + this.memoryCache.delete(key); + await this.redisClient.del(`kvcache:${this.name}:${key}`); + } + + /** + * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します + * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします + */ + @bindThis + public async fetch(key: string, fetcher: () => Promise, validator?: (cachedValue: Serialized | T) => boolean): Promise | T> { + const cachedValue = await this.get(key); + if (cachedValue !== undefined) { + if (validator) { + if (validator(cachedValue)) { + // Cache HIT + return cachedValue; + } + } else { + // Cache HIT + return cachedValue; + } + } + + // Cache MISS + const value = await fetcher(); + this.set(key, value); + return value; + } +} + // TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする? export class MemoryKVCache { - public cache: Map; + public cache: Map; private lifetime: number; constructor(lifetime: MemoryKVCache['lifetime']) { @@ -12,7 +97,7 @@ export class MemoryKVCache { } @bindThis - public set(key: string | null, value: T): void { + public set(key: string, value: T): void { this.cache.set(key, { date: Date.now(), value, @@ -20,7 +105,7 @@ export class MemoryKVCache { } @bindThis - public get(key: string | null): T | undefined { + public get(key: string): T | undefined { const cached = this.cache.get(key); if (cached == null) return undefined; if ((Date.now() - cached.date) > this.lifetime) { @@ -31,7 +116,7 @@ export class MemoryKVCache { } @bindThis - public delete(key: string | null) { + public delete(key: string) { this.cache.delete(key); } @@ -40,7 +125,7 @@ export class MemoryKVCache { * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします */ @bindThis - public async fetch(key: string | null, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { + public async fetch(key: string, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { @@ -65,7 +150,7 @@ export class MemoryKVCache { * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします */ @bindThis - public async fetchMaybe(key: string | null, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { + public async fetchMaybe(key: string, fetcher: () => Promise, validator?: (cachedValue: T) => boolean): Promise { const cachedValue = this.get(key); if (cachedValue !== undefined) { if (validator) { -- cgit v1.2.3-freya