diff --git a/package.json b/package.json index b7d2b7053..79fdd4999 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "jmespath": "0.16.0", "js-yaml": "5.4.1", "jsonwebtoken": "9.0.3", + "lru-cache": "11.5.2", "lucide-react": "1.38.0", "maxmind": "5.0.7", "moment": "2.30.1", @@ -103,7 +104,6 @@ "next-intl": "4.14.1", "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", - "node-cache": "5.1.2", "nodemailer": "9.1.0", "oslo": "1.2.1", "pg": "8.23.0", diff --git a/server/lib/cache.ts b/server/lib/cache.ts index 1aece08ac..a039e305d 100644 --- a/server/lib/cache.ts +++ b/server/lib/cache.ts @@ -1,13 +1,7 @@ -import NodeCache from "node-cache"; import logger from "@server/logger"; +import { createLocalCache } from "@server/lib/createLocalCache"; -// Create local cache with maxKeys limit to prevent memory leaks -// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient -export const localCache = new NodeCache({ - stdTTL: 3600, - checkperiod: 120, - maxKeys: 10000 -}); +export const localCache = createLocalCache(); // Log cache statistics periodically for monitoring // setInterval(() => { diff --git a/server/lib/createLocalCache.ts b/server/lib/createLocalCache.ts new file mode 100644 index 000000000..f37fc48ad --- /dev/null +++ b/server/lib/createLocalCache.ts @@ -0,0 +1,79 @@ +import { LRUCache } from "lru-cache"; + +const DEFAULT_MAX_KEYS = 10000; +const DEFAULT_TTL_MS = 3600 * 1000; + +export type LocalCache = { + get(key: string): T | undefined; + set(key: string, value: unknown, ttlSeconds?: number): boolean; + del(key: string | string[]): number; + has(key: string): boolean; + keys(): string[]; + flushAll(): void; + getStats(): { keys: number }; + getTtl(key: string): number | undefined; +}; + +export function createLocalCache(max = DEFAULT_MAX_KEYS): LocalCache { + const lru = new LRUCache({ + max, + ttl: DEFAULT_TTL_MS, + updateAgeOnGet: false + }); + + return { + get(key: string): T | undefined { + return lru.get(key) as T | undefined; + }, + + set(key: string, value: unknown, ttlSeconds?: number): boolean { + const stored = value as {}; + if (ttlSeconds === undefined) { + lru.set(key, stored); + } else if (ttlSeconds === 0) { + lru.set(key, stored, { ttl: 0 }); + } else { + lru.set(key, stored, { ttl: ttlSeconds * 1000 }); + } + return true; + }, + + del(key: string | string[]): number { + const keys = Array.isArray(key) ? key : [key]; + let deleted = 0; + for (const k of keys) { + if (lru.delete(k)) { + deleted++; + } + } + return deleted; + }, + + has(key: string): boolean { + return lru.has(key); + }, + + keys(): string[] { + return [...lru.keys()]; + }, + + flushAll(): void { + lru.clear(); + }, + + getStats(): { keys: number } { + return { keys: lru.size }; + }, + + getTtl(key: string): number | undefined { + if (!lru.has(key)) { + return undefined; + } + const remaining = lru.getRemainingTTL(key); + if (!Number.isFinite(remaining)) { + return 0; + } + return Date.now() + remaining; + } + }; +} diff --git a/server/private/lib/cache.ts b/server/private/lib/cache.ts index 7ddfeb554..c908b50e6 100644 --- a/server/private/lib/cache.ts +++ b/server/private/lib/cache.ts @@ -11,17 +11,11 @@ * This file is not licensed under the AGPLv3. */ -import NodeCache from "node-cache"; import logger from "@server/logger"; +import { createLocalCache } from "@server/lib/createLocalCache"; import { redisManager, regionalRedisManager } from "@server/private/lib/redis"; -// Create local cache with maxKeys limit to prevent memory leaks -// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient -export const localCache = new NodeCache({ - stdTTL: 3600, - checkperiod: 120, - maxKeys: 10000 -}); +export const localCache = createLocalCache(); // Log cache statistics periodically for monitoring // setInterval(() => { @@ -301,15 +295,11 @@ export default cache; /** * Regional adaptive cache backed by the in-cluster Redis instance. - * Falls back to a local NodeCache when the regional Redis is unavailable. + * Falls back to a local LRU cache when the regional Redis is unavailable. * Use this for data that is regional in nature (e.g. status history) so * reads are served from the same cluster the user is hitting. */ -const regionalLocalCache = new NodeCache({ - stdTTL: 3600, - checkperiod: 120, - maxKeys: 10000 -}); +const regionalLocalCache = createLocalCache(); class RegionalAdaptiveCache { private useRedis(): boolean { diff --git a/server/private/license/license.ts b/server/private/license/license.ts index e92fcf844..4442a40c2 100644 --- a/server/private/license/license.ts +++ b/server/private/license/license.ts @@ -14,7 +14,7 @@ import { db, HostMeta, sites, users } from "@server/db"; import { hostMeta, licenseKey } from "@server/db"; import logger from "@server/logger"; -import NodeCache from "node-cache"; +import { createLocalCache } from "@server/lib/createLocalCache"; import { validateJWT } from "./licenseJwt"; import { count, eq } from "drizzle-orm"; import moment from "moment"; @@ -65,8 +65,8 @@ export class License { private validationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/validate`; private activationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/activate`; - private statusCache = new NodeCache(); - private licenseKeyCache = new NodeCache(); + private statusCache = createLocalCache(); + private licenseKeyCache = createLocalCache(); private statusKey = "status"; private serverSecret!: string; @@ -179,7 +179,7 @@ LQIDAQAB status.isHostLicensed = false; // Invalidate all and set new cache (empty) this.licenseKeyCache.flushAll(); - this.statusCache.set(this.statusKey, status); + this.statusCache.set(this.statusKey, status, 0); return status; } @@ -389,7 +389,7 @@ LQIDAQAB // Invalidate old cache and set new cache this.licenseKeyCache.flushAll(); for (const [key, value] of newCache.entries()) { - this.licenseKeyCache.set(key, value); + this.licenseKeyCache.set(key, value, 0); } } catch (error) { logger.error("Error checking license status:"); @@ -398,7 +398,7 @@ LQIDAQAB this.checkInProgress = false; } - this.statusCache.set(this.statusKey, status); + this.statusCache.set(this.statusKey, status, 0); return status; } diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index f50cf8cdc..486feb2b7 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -691,7 +691,9 @@ export async function verifyResourceSession( ); resourceSession = result?.resourceSession; - localCache.set(sessionCacheKey, resourceSession, 5); + if (resourceSession) { + localCache.set(sessionCacheKey, resourceSession, 5); + } } if (resourceSession?.isRequestToken) { @@ -1121,7 +1123,9 @@ async function allowAccessToken( resource.resourceId ); resourceSession = result?.resourceSession; - localCache.set(sessionCacheKey, resourceSession, 5); + if (resourceSession) { + localCache.set(sessionCacheKey, resourceSession, 5); + } } if (