From 893d273a2b262c6a005b79f598af8874fee264cc Mon Sep 17 00:00:00 2001 From: CCristi Date: Mon, 24 Jun 2024 13:14:44 +0200 Subject: [PATCH 1/4] feat: Add LRU cache alongside redis --- package.json | 2 +- packages/explorerkit-server/package.json | 1 + .../src/components/decoders/errors.spec.ts | 18 ++- .../src/components/decoders/errors.ts | 4 +- .../components/decoders/instructions.spec.ts | 18 ++- .../src/components/decoders/instructions.ts | 6 +- .../explorerkit-server/src/components/idls.ts | 11 +- packages/explorerkit-server/src/core/cache.ts | 130 +++++++++++++----- .../explorerkit-server/src/server.test.ts | 18 ++- packages/explorerkit-server/src/server.ts | 28 ++-- .../src/parsers/v2/account/token.ts | 1 - pnpm-lock.yaml | 52 ++++--- 12 files changed, 208 insertions(+), 81 deletions(-) diff --git a/package.json b/package.json index 858f743..cec3060 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "rimraf": "^5.0.5", "tsconfig": "workspace:*", "tsup": "^7.2.0", - "turbo": "latest" + "turbo": "^1.13.4" }, "packageManager": "pnpm@8.6.10", "name": "explorer-kit" diff --git a/packages/explorerkit-server/package.json b/packages/explorerkit-server/package.json index a27a445..f56f277 100644 --- a/packages/explorerkit-server/package.json +++ b/packages/explorerkit-server/package.json @@ -50,6 +50,7 @@ "bs58": "^5.0.0", "dotenv": "^16.4.5", "express": "^4.18.2", + "lru-cache": "^10.2.2", "prom-client": "^15.1.0", "redis": "^4.6.14", "vite": "^5.2.13", diff --git a/packages/explorerkit-server/src/components/decoders/errors.spec.ts b/packages/explorerkit-server/src/components/decoders/errors.spec.ts index 7e7c870..920bb19 100644 --- a/packages/explorerkit-server/src/components/decoders/errors.spec.ts +++ b/packages/explorerkit-server/src/components/decoders/errors.spec.ts @@ -4,8 +4,24 @@ import { decodeProgramError } from "@/components/decoders/errors"; import { loadAllIdls } from "@/components/idls"; vi.mock("@/core/shared-dependencies", (loadActual) => { + class MultiCacheMock { + private data: Record = {}; + + async get(key: string) { + return this.data[key] || null; + } + + async multiGet(keys: string[]) { + return keys.map((key) => this.data[key] || null); + } + + async set(key: string, value: string) { + this.data[key] = value; + } + } + const deps = { - cache: new Map(), + cache: new MultiCacheMock(), }; return { diff --git a/packages/explorerkit-server/src/components/decoders/errors.ts b/packages/explorerkit-server/src/components/decoders/errors.ts index 37a8afa..c31807b 100644 --- a/packages/explorerkit-server/src/components/decoders/errors.ts +++ b/packages/explorerkit-server/src/components/decoders/errors.ts @@ -1,4 +1,4 @@ -import { ParserType } from "@solanafm/explorer-kit"; +import { checkIfErrorsParser, ParserType } from "@solanafm/explorer-kit"; import { IdlsMap } from "@/components/idls"; import { ProgramError } from "@/types"; @@ -18,7 +18,7 @@ export function decodeProgramError(idls: IdlsMap, programError: ProgramError): P const hexErrorCode = `0x${programError.errorCode.toString(16)}`; const errorParser = parser.createParser(ParserType.ERROR); - if (!errorParser || !("parseError" in errorParser)) { + if (!errorParser || !checkIfErrorsParser(errorParser)) { return programError; } diff --git a/packages/explorerkit-server/src/components/decoders/instructions.spec.ts b/packages/explorerkit-server/src/components/decoders/instructions.spec.ts index 5638cce..850edf9 100644 --- a/packages/explorerkit-server/src/components/decoders/instructions.spec.ts +++ b/packages/explorerkit-server/src/components/decoders/instructions.spec.ts @@ -4,8 +4,24 @@ import { decodeInstruction } from "@/components/decoders/instructions"; import { loadAllIdls } from "@/components/idls"; vi.mock("@/core/shared-dependencies", (loadActual) => { + class MultiCacheMock { + private data: Record = {}; + + async get(key: string) { + return this.data[key] || null; + } + + async multiGet(keys: string[]) { + return keys.map((key) => this.data[key] || null); + } + + async set(key: string, value: string) { + this.data[key] = value; + } + } + const deps = { - cache: new Map(), + cache: new MultiCacheMock(), }; return { diff --git a/packages/explorerkit-server/src/components/decoders/instructions.ts b/packages/explorerkit-server/src/components/decoders/instructions.ts index 087e083..768102c 100644 --- a/packages/explorerkit-server/src/components/decoders/instructions.ts +++ b/packages/explorerkit-server/src/components/decoders/instructions.ts @@ -1,10 +1,10 @@ -import { ParserType } from "@solanafm/explorer-kit"; +import { checkIfInstructionParser, ParserType } from "@solanafm/explorer-kit"; import { Buffer } from "buffer"; import { IdlsMap } from "@/components/idls"; import { Instruction, TopLevelInstruction } from "@/types"; -export async function decodeInstruction(idls: IdlsMap, instruction: Instruction): Promise { +export function decodeInstruction(idls: IdlsMap, instruction: Instruction): Instruction { const programId = instruction.programId.toString(); let parsedInstruction = { programId: programId.toString(), @@ -20,7 +20,7 @@ export async function decodeInstruction(idls: IdlsMap, instruction: Instruction) } let instructionParser = parser.createParser(ParserType.INSTRUCTION); - if (!instructionParser || !("parseInstructions" in instructionParser)) { + if (!instructionParser || !checkIfInstructionParser(instructionParser)) { return parsedInstruction; // Short-circuit without decodedData parser can't be created } diff --git a/packages/explorerkit-server/src/components/idls.ts b/packages/explorerkit-server/src/components/idls.ts index fcffbac..95eae4b 100644 --- a/packages/explorerkit-server/src/components/idls.ts +++ b/packages/explorerkit-server/src/components/idls.ts @@ -9,15 +9,20 @@ const IDL_CACHE_TTL = 3600; // one hour export async function loadAllIdls(programIds: string[]): Promise { const idls: IdlsMap = new Map(); + + if (programIds.length === 0) { + return idls; + } + const cache = getSharedDep("cache"); - const cachedIdls = await Promise.allSettled(programIds.map((id) => cache.get(id))); + const cachedIdls = await cache.multiGet(programIds); await Promise.allSettled( cachedIdls.map(async (res, i) => { const programId = programIds[i]!; - if (res.status === "fulfilled" && res.value) { - const idl = deserializeIdl(res.value); + if (res) { + const idl = deserializeIdl(res); idls.set(programId, idl && new SolanaFMParser(idl, programId)); return; } diff --git a/packages/explorerkit-server/src/core/cache.ts b/packages/explorerkit-server/src/core/cache.ts index 806124c..424cb3c 100644 --- a/packages/explorerkit-server/src/core/cache.ts +++ b/packages/explorerkit-server/src/core/cache.ts @@ -1,3 +1,4 @@ +import { LRUCache } from "lru-cache"; import { Gauge } from "prom-client"; import { createClient, RedisClientType } from "redis"; @@ -5,54 +6,107 @@ import { register } from "@/components/metrics"; import { config } from "@/core/config"; import { onTeardown } from "@/utils/teardown"; -export async function createCache(): Promise { - const client = createClient({ - url: config.REDIS_URL, - }); +const LRU_CACHE_MAX_ITEMS_COUNT = 100; - await client.connect(); +type CacheMetricGauges = { + hits: Gauge; + misses: Gauge; +}; - onTeardown(async () => { - await client.disconnect(); - }); +class MultiCache { + constructor( + private redis: RedisClientType, + private lruCache: LRUCache, + private guages: CacheMetricGauges + ) {} - return instrumentClient(client as RedisClientType); -} + async get(key: string): Promise { + const item = this.lruCache.get(key) ?? (await this.redis.get(key)); -const instrumentClient = (client: RedisClientType): RedisClientType => { - const hitsGauge = new Gauge({ - name: "cache_hits_total", - help: "Total number of cache hits", - registers: [register], - }); + if (item) { + this.guages.hits.inc(); + } else { + this.guages.misses.inc(); + } - const missesGauge = new Gauge({ - name: "cache_misses_total", - help: "Total number of cache misses", - registers: [register], - }); + return item; + } + + async multiGet(keys: string[]): Promise<(string | null)[]> { + const items: Record = {}; + const missingLruKeys: string[] = []; - return new Proxy(client, { - get(target, prop, receiver) { - if (prop === "get") { - return async (key: string) => { - const value = await target.get(key); + for (const key of keys) { + const value = this.lruCache.get(key); + + if (value) { + items[key] = value; + } else { + missingLruKeys.push(key); + } + } - if (value) { - hitsGauge.inc(); - } else { - missesGauge.inc(); - } + if (missingLruKeys.length > 0) { + const redisItems = await this.redis.mGet(missingLruKeys); - return value; - }; + for (const [i, maybeIdl] of redisItems.entries()) { + const key = missingLruKeys[i]!; + items[key] = maybeIdl; + if (maybeIdl) { + this.lruCache.set(key, maybeIdl); + } } + } - const value = Reflect.get(target, prop, receiver); + return keys.map((key) => { + const item = items[key]; - if (typeof value === "function") { - return value.bind(target); + if (item) { + this.guages.hits.inc(); + } else { + this.guages.misses.inc(); } - }, + + return item ?? null; + }); + } + + async set(key: string, value: string, options: { EX: number }): Promise { + this.lruCache.set(key, value); + await this.redis.set(key, value, options); + } + + async teardown() { + await this.redis.disconnect(); + } +} + +export async function createCache(): Promise { + const redisClient = createClient({ + url: config.REDIS_URL, }); -}; + await redisClient.connect(); + + const lruCache = new LRUCache({ + max: LRU_CACHE_MAX_ITEMS_COUNT, + }); + + const multiCache = new MultiCache(redisClient as RedisClientType, lruCache, { + hits: new Gauge({ + name: "cache_hits_total", + help: "Total number of cache hits", + registers: [register], + }), + misses: new Gauge({ + name: "cache_misses_total", + help: "Total number of cache misses", + registers: [register], + }), + }); + + onTeardown(async () => { + await multiCache.teardown(); + }); + + return multiCache; +} diff --git a/packages/explorerkit-server/src/server.test.ts b/packages/explorerkit-server/src/server.test.ts index d8939db..23ffb65 100644 --- a/packages/explorerkit-server/src/server.test.ts +++ b/packages/explorerkit-server/src/server.test.ts @@ -4,8 +4,24 @@ import { describe, expect, it, vi } from "vitest"; import { app } from "@/server"; vi.mock("@/core/shared-dependencies", (loadActual) => { + class MultiCacheMock { + private data: Record = {}; + + async get(key: string) { + return this.data[key] || null; + } + + async multiGet(keys: string[]) { + return keys.map((key) => this.data[key] || null); + } + + async set(key: string, value: string) { + this.data[key] = value; + } + } + const deps = { - cache: new Map(), + cache: new MultiCacheMock(), }; return { diff --git a/packages/explorerkit-server/src/server.ts b/packages/explorerkit-server/src/server.ts index e00b63c..8d0e20b 100644 --- a/packages/explorerkit-server/src/server.ts +++ b/packages/explorerkit-server/src/server.ts @@ -1,4 +1,4 @@ -import { AccountParserInterface, ParserType } from "@solanafm/explorer-kit"; +import { checkIfAccountParser, ParserType } from "@solanafm/explorer-kit"; import bodyParser from "body-parser"; import express, { Express, Request, Response } from "express"; import { collectDefaultMetrics } from "prom-client"; @@ -76,7 +76,13 @@ app.post("/decode/accounts", responseDurationMiddleware, async (req: Request, re } // Parse the account - let accountParser = parser.createParser(ParserType.ACCOUNT) as AccountParserInterface; + const accountParser = parser.createParser(ParserType.ACCOUNT); + + if (!accountParser || !checkIfAccountParser(accountParser)) { + decodedAccounts.push({ decodedData: null }); + continue; + } + const decodedData = accountParser.parseAccount(account.data); decodedAccounts.push({ decodedData: decodedData @@ -96,7 +102,9 @@ const decodeErrorsSchema = z.object({ errors: z.array( z .object({ - programId: z.string(), + programId: z.string().refine(isValidBase58, { + message: "error.programId is not a valid base58 string", + }), errorCode: z.coerce.number().nullable().optional(), }) .nullable() @@ -132,13 +140,17 @@ const decodeInstructionsSchema = z.object({ .array( z.object({ topLevelInstruction: z.object({ - programId: z.string(), + programId: z.string().refine(isValidBase58, { + message: "topLevelInstruction.programId is not a valid base58 string", + }), encodedData: z.string(), accountKeys: z.array(z.string()), }), flattenedInnerInstructions: z.array( z.object({ - programId: z.string(), + programId: z.string().refine(isValidBase58, { + message: "flattenedInnerInstructions.programId is not a valid base58 string", + }), encodedData: z.string(), accountKeys: z.array(z.string()), }) @@ -174,11 +186,11 @@ app.post("/decode/instructions", responseDurationMiddleware, async (req: Request for (const instruction of transactionInstructions) { // First decode top level ix, then all nested ixs - const decodedTopLevelInstruction = await decodeInstruction(idls, instruction.topLevelInstruction); + const decodedTopLevelInstruction = decodeInstruction(idls, instruction.topLevelInstruction); const decodedInnerInstruction = []; for (const innerInstruction of instruction.flattenedInnerInstructions) { - decodedInnerInstruction.push(await decodeInstruction(idls, innerInstruction)); + decodedInnerInstruction.push(decodeInstruction(idls, innerInstruction)); } decodedTransaction.push({ @@ -191,7 +203,7 @@ app.post("/decode/instructions", responseDurationMiddleware, async (req: Request return res.status(200).json({ decodedTransactions }); } catch (e: any) { - console.error("failed to decode instructions", e); + console.error("failed to decode instructions", e, JSON.stringify(data)); return res.status(500).json({ error: e.message }); } }); diff --git a/packages/explorerkit-translator/src/parsers/v2/account/token.ts b/packages/explorerkit-translator/src/parsers/v2/account/token.ts index 4279403..e7e3d59 100644 --- a/packages/explorerkit-translator/src/parsers/v2/account/token.ts +++ b/packages/explorerkit-translator/src/parsers/v2/account/token.ts @@ -87,7 +87,6 @@ export const createShankTokenAccount: (idlItem: IdlItem) => AccountParserInterfa return null; } catch (error) { - console.error(error); return null; } }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68ed4ef..88de455 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^7.2.0 version: 7.2.0 turbo: - specifier: latest - version: 2.0.3 + specifier: ^1.13.4 + version: 1.13.4 packages/eslint-config-explorerkit: dependencies: @@ -102,6 +102,9 @@ importers: express: specifier: ^4.18.2 version: 4.18.2 + lru-cache: + specifier: ^10.2.2 + version: 10.2.2 prom-client: specifier: ^15.1.0 version: 15.1.0 @@ -3884,6 +3887,11 @@ packages: engines: {node: 14 || >=16.14} dev: true + /lru-cache@10.2.2: + resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==} + engines: {node: 14 || >=16.14} + dev: false + /lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} dependencies: @@ -5315,64 +5323,64 @@ packages: yargs: 17.7.2 dev: true - /turbo-darwin-64@2.0.3: - resolution: {integrity: sha512-v7ztJ8sxdHw3SLfO2MhGFeeU4LQhFii1hIGs9uBiXns/0YTGOvxLeifnfGqhfSrAIIhrCoByXO7nR9wlm10n3Q==} + /turbo-darwin-64@1.13.4: + resolution: {integrity: sha512-A0eKd73R7CGnRinTiS7txkMElg+R5rKFp9HV7baDiEL4xTG1FIg/56Vm7A5RVgg8UNgG2qNnrfatJtb+dRmNdw==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@2.0.3: - resolution: {integrity: sha512-LUcqvkV9Bxtng6QHbevp8IK8zzwbIxM6HMjCE7FEW6yJBN1KwvTtRtsGBwwmTxaaLO0wD1Jgl3vgkXAmQ4fqUw==} + /turbo-darwin-arm64@1.13.4: + resolution: {integrity: sha512-eG769Q0NF6/Vyjsr3mKCnkG/eW6dKMBZk6dxWOdrHfrg6QgfkBUk0WUUujzdtVPiUIvsh4l46vQrNVd9EOtbyA==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@2.0.3: - resolution: {integrity: sha512-xpdY1suXoEbsQsu0kPep2zrB8ijv/S5aKKrntGuQ62hCiwDFoDcA/Z7FZ8IHQ2u+dpJARa7yfiByHmizFE0r5Q==} + /turbo-linux-64@1.13.4: + resolution: {integrity: sha512-Bq0JphDeNw3XEi+Xb/e4xoKhs1DHN7OoLVUbTIQz+gazYjigVZvtwCvgrZI7eW9Xo1eOXM2zw2u1DGLLUfmGkQ==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@2.0.3: - resolution: {integrity: sha512-MBACTcSR874L1FtLL7gkgbI4yYJWBUCqeBN/iE29D+8EFe0d3fAyviFlbQP4K/HaDYet1i26xkkOiWr0z7/V9A==} + /turbo-linux-arm64@1.13.4: + resolution: {integrity: sha512-BJcXw1DDiHO/okYbaNdcWN6szjXyHWx9d460v6fCHY65G8CyqGU3y2uUTPK89o8lq/b2C8NK0yZD+Vp0f9VoIg==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@2.0.3: - resolution: {integrity: sha512-zi3YuKPkM9JxMTshZo3excPk37hUrj5WfnCqh4FjI26ux6j/LJK+Dh3SebMHd9mR7wP9CMam4GhmLCT+gDfM+w==} + /turbo-windows-64@1.13.4: + resolution: {integrity: sha512-OFFhXHOFLN7A78vD/dlVuuSSVEB3s9ZBj18Tm1hk3aW1HTWTuAw0ReN6ZNlVObZUHvGy8d57OAGGxf2bT3etQw==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@2.0.3: - resolution: {integrity: sha512-wmed4kkenLvRbidi7gISB4PU77ujBuZfgVGDZ4DXTFslE/kYpINulwzkVwJIvNXsJtHqyOq0n6jL8Zwl3BrwDg==} + /turbo-windows-arm64@1.13.4: + resolution: {integrity: sha512-u5A+VOKHswJJmJ8o8rcilBfU5U3Y1TTAfP9wX8bFh8teYF1ghP0EhtMRLjhtp6RPa+XCxHHVA2CiC3gbh5eg5g==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@2.0.3: - resolution: {integrity: sha512-jF1K0tTUyryEWmgqk1V0ALbSz3VdeZ8FXUo6B64WsPksCMCE48N5jUezGOH2MN0+epdaRMH8/WcPU0QQaVfeLA==} + /turbo@1.13.4: + resolution: {integrity: sha512-1q7+9UJABuBAHrcC4Sxp5lOqYS5mvxRrwa33wpIyM18hlOCpRD/fTJNxZ0vhbMcJmz15o9kkVm743mPn7p6jpQ==} hasBin: true optionalDependencies: - turbo-darwin-64: 2.0.3 - turbo-darwin-arm64: 2.0.3 - turbo-linux-64: 2.0.3 - turbo-linux-arm64: 2.0.3 - turbo-windows-64: 2.0.3 - turbo-windows-arm64: 2.0.3 + turbo-darwin-64: 1.13.4 + turbo-darwin-arm64: 1.13.4 + turbo-linux-64: 1.13.4 + turbo-linux-arm64: 1.13.4 + turbo-windows-64: 1.13.4 + turbo-windows-arm64: 1.13.4 dev: true /type-check@0.4.0: From 29e2e41bed9d00b24cd87fa472fb53d52b409be7 Mon Sep 17 00:00:00 2001 From: CCristi Date: Mon, 24 Jun 2024 13:45:30 +0200 Subject: [PATCH 2/4] feat: Add separate gauge for lru hits --- packages/explorerkit-server/src/core/cache.ts | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/packages/explorerkit-server/src/core/cache.ts b/packages/explorerkit-server/src/core/cache.ts index 424cb3c..7fe86a2 100644 --- a/packages/explorerkit-server/src/core/cache.ts +++ b/packages/explorerkit-server/src/core/cache.ts @@ -9,7 +9,8 @@ import { onTeardown } from "@/utils/teardown"; const LRU_CACHE_MAX_ITEMS_COUNT = 100; type CacheMetricGauges = { - hits: Gauge; + redisHits: Gauge; + lruHits: Gauge; misses: Gauge; }; @@ -20,18 +21,6 @@ class MultiCache { private guages: CacheMetricGauges ) {} - async get(key: string): Promise { - const item = this.lruCache.get(key) ?? (await this.redis.get(key)); - - if (item) { - this.guages.hits.inc(); - } else { - this.guages.misses.inc(); - } - - return item; - } - async multiGet(keys: string[]): Promise<(string | null)[]> { const items: Record = {}; const missingLruKeys: string[] = []; @@ -41,6 +30,7 @@ class MultiCache { if (value) { items[key] = value; + this.guages.lruHits.inc(); } else { missingLruKeys.push(key); } @@ -53,22 +43,15 @@ class MultiCache { const key = missingLruKeys[i]!; items[key] = maybeIdl; if (maybeIdl) { + this.guages.redisHits.inc(); this.lruCache.set(key, maybeIdl); + } else { + this.guages.misses.inc(); } } } - return keys.map((key) => { - const item = items[key]; - - if (item) { - this.guages.hits.inc(); - } else { - this.guages.misses.inc(); - } - - return item ?? null; - }); + return keys.map((key) => items[key] ?? null); } async set(key: string, value: string, options: { EX: number }): Promise { @@ -92,9 +75,14 @@ export async function createCache(): Promise { }); const multiCache = new MultiCache(redisClient as RedisClientType, lruCache, { - hits: new Gauge({ - name: "cache_hits_total", - help: "Total number of cache hits", + redisHits: new Gauge({ + name: "redis_cache_hits_total", + help: "Total number of redis cache hits", + registers: [register], + }), + lruHits: new Gauge({ + name: "lru_cache_hits_total", + help: "Total number of lru-cache hits", registers: [register], }), misses: new Gauge({ From 62f4e56fc725fe0baa70a6255811b30620df25d7 Mon Sep 17 00:00:00 2001 From: CCristi Date: Mon, 24 Jun 2024 15:40:23 +0200 Subject: [PATCH 3/4] chore: Use ttl for lru cache as well --- .../explorerkit-server/src/components/idls.ts | 4 ++-- packages/explorerkit-server/src/core/cache.ts | 20 +++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/explorerkit-server/src/components/idls.ts b/packages/explorerkit-server/src/components/idls.ts index 95eae4b..b5c2c54 100644 --- a/packages/explorerkit-server/src/components/idls.ts +++ b/packages/explorerkit-server/src/components/idls.ts @@ -15,7 +15,7 @@ export async function loadAllIdls(programIds: string[]): Promise { } const cache = getSharedDep("cache"); - const cachedIdls = await cache.multiGet(programIds); + const cachedIdls = await cache.multiGet(programIds, IDL_CACHE_TTL); await Promise.allSettled( cachedIdls.map(async (res, i) => { @@ -28,7 +28,7 @@ export async function loadAllIdls(programIds: string[]): Promise { } const idl = await getProgramIdl(programId); - void cache.set(programId, serializeIdl(idl), { EX: IDL_CACHE_TTL }); + void cache.set(programId, serializeIdl(idl), IDL_CACHE_TTL); idls.set(programId, idl && new SolanaFMParser(idl, programId)); }) ); diff --git a/packages/explorerkit-server/src/core/cache.ts b/packages/explorerkit-server/src/core/cache.ts index 7fe86a2..f26f21c 100644 --- a/packages/explorerkit-server/src/core/cache.ts +++ b/packages/explorerkit-server/src/core/cache.ts @@ -6,7 +6,7 @@ import { register } from "@/components/metrics"; import { config } from "@/core/config"; import { onTeardown } from "@/utils/teardown"; -const LRU_CACHE_MAX_ITEMS_COUNT = 100; +const LRU_CACHE_MAX_ITEMS_COUNT = 1000; type CacheMetricGauges = { redisHits: Gauge; @@ -21,7 +21,7 @@ class MultiCache { private guages: CacheMetricGauges ) {} - async multiGet(keys: string[]): Promise<(string | null)[]> { + async multiGet(keys: string[], ttlInS: number = 0): Promise<(string | null)[]> { const items: Record = {}; const missingLruKeys: string[] = []; @@ -44,7 +44,9 @@ class MultiCache { items[key] = maybeIdl; if (maybeIdl) { this.guages.redisHits.inc(); - this.lruCache.set(key, maybeIdl); + this.lruCache.set(key, maybeIdl, { + ttl: ttlInS * 1000, + }); } else { this.guages.misses.inc(); } @@ -54,9 +56,14 @@ class MultiCache { return keys.map((key) => items[key] ?? null); } - async set(key: string, value: string, options: { EX: number }): Promise { - this.lruCache.set(key, value); - await this.redis.set(key, value, options); + async set(key: string, value: string, ttlInS: number = 0): Promise { + this.lruCache.set(key, value, { + ttl: ttlInS * 1000, + }); + + await this.redis.set(key, value, { + EX: ttlInS, + }); } async teardown() { @@ -72,6 +79,7 @@ export async function createCache(): Promise { const lruCache = new LRUCache({ max: LRU_CACHE_MAX_ITEMS_COUNT, + updateAgeOnGet: true, }); const multiCache = new MultiCache(redisClient as RedisClientType, lruCache, { From c27555d0b07e5b44d8133018fe64ca1465e18c1d Mon Sep 17 00:00:00 2001 From: CCristi Date: Tue, 25 Jun 2024 11:27:06 +0200 Subject: [PATCH 4/4] chore: Use 100 items in LRU cache instead --- packages/explorerkit-server/src/core/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/explorerkit-server/src/core/cache.ts b/packages/explorerkit-server/src/core/cache.ts index f26f21c..f8de01d 100644 --- a/packages/explorerkit-server/src/core/cache.ts +++ b/packages/explorerkit-server/src/core/cache.ts @@ -6,7 +6,7 @@ import { register } from "@/components/metrics"; import { config } from "@/core/config"; import { onTeardown } from "@/utils/teardown"; -const LRU_CACHE_MAX_ITEMS_COUNT = 1000; +const LRU_CACHE_MAX_ITEMS_COUNT = 100; type CacheMetricGauges = { redisHits: Gauge;