summaryrefslogtreecommitdiff
path: root/packages/backend/src/misc
diff options
context:
space:
mode:
authorAcid Chicken (硫酸鶏) <root@acid-chicken.com>2023-04-05 00:41:49 +0900
committerGitHub <noreply@github.com>2023-04-05 00:41:49 +0900
commit7bd0001e763a12c2b2aeb5cf4417f802cd4fbb4c (patch)
tree62ca232417372612f78761f26669b56a80d35733 /packages/backend/src/misc
parentMerge branch 'develop' into fix/visibility-widening (diff)
parentenhance(backend): improve cache (diff)
downloadsharkey-7bd0001e763a12c2b2aeb5cf4417f802cd4fbb4c.tar.gz
sharkey-7bd0001e763a12c2b2aeb5cf4417f802cd4fbb4c.tar.bz2
sharkey-7bd0001e763a12c2b2aeb5cf4417f802cd4fbb4c.zip
Merge branch 'develop' into fix/visibility-widening
Diffstat (limited to 'packages/backend/src/misc')
-rw-r--r--packages/backend/src/misc/cache.ts105
1 files changed, 95 insertions, 10 deletions
diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts
index b249cf4480..870dfd237c 100644
--- a/packages/backend/src/misc/cache.ts
+++ b/packages/backend/src/misc/cache.ts
@@ -1,18 +1,103 @@
+import Redis from 'ioredis';
import { bindThis } from '@/decorators.js';
+// redis通すとDateのインスタンスはstringに変換されるので
+type Serialized<T> = {
+ [K in keyof T]:
+ T[K] extends Date
+ ? string
+ : T[K] extends (Date | null)
+ ? (string | null)
+ : T[K] extends Record<string, any>
+ ? Serialized<T[K]>
+ : T[K];
+};
+
+export class RedisKVCache<T> {
+ private redisClient: Redis.Redis;
+ private name: string;
+ private lifetime: number;
+ private memoryCache: MemoryKVCache<T>;
+
+ constructor(redisClient: RedisKVCache<never>['redisClient'], name: RedisKVCache<never>['name'], lifetime: RedisKVCache<never>['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<void> {
+ 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<Serialized<T> | 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<void> {
+ this.memoryCache.delete(key);
+ await this.redisClient.del(`kvcache:${this.name}:${key}`);
+ }
+
+ /**
+ * キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
+ * optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
+ */
+ @bindThis
+ public async fetch(key: string, fetcher: () => Promise<T>, validator?: (cachedValue: Serialized<T> | T) => boolean): Promise<Serialized<T> | 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 KVCache<T> {
- public cache: Map<string | null, { date: number; value: T; }>;
+export class MemoryKVCache<T> {
+ public cache: Map<string, { date: number; value: T; }>;
private lifetime: number;
- constructor(lifetime: KVCache<never>['lifetime']) {
+ constructor(lifetime: MemoryKVCache<never>['lifetime']) {
this.cache = new Map();
this.lifetime = lifetime;
}
@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 KVCache<T> {
}
@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 KVCache<T> {
}
@bindThis
- public delete(key: string | null) {
+ public delete(key: string) {
this.cache.delete(key);
}
@@ -40,7 +125,7 @@ export class KVCache<T> {
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
@bindThis
- public async fetch(key: string | null, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
+ public async fetch(key: string, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
const cachedValue = this.get(key);
if (cachedValue !== undefined) {
if (validator) {
@@ -65,7 +150,7 @@ export class KVCache<T> {
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
*/
@bindThis
- public async fetchMaybe(key: string | null, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
+ public async fetchMaybe(key: string, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
const cachedValue = this.get(key);
if (cachedValue !== undefined) {
if (validator) {
@@ -88,12 +173,12 @@ export class KVCache<T> {
}
}
-export class Cache<T> {
+export class MemoryCache<T> {
private cachedAt: number | null = null;
private value: T | undefined;
private lifetime: number;
- constructor(lifetime: Cache<never>['lifetime']) {
+ constructor(lifetime: MemoryCache<never>['lifetime']) {
this.lifetime = lifetime;
}