diff --git a/backend/.env.example b/backend/.env.example index cbdcc7e..447b9f8 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -49,3 +49,9 @@ ADMIN_PUBLIC_KEY= # Leave empty to run in single-instance mode (no Redis required). REDIS_URL= +# ─── Caching (optional) ─────────────────────────────────────────────────────── +# Time in milliseconds between periodic sweeps to prune expired memory cache +# entries (default: 60000) +MEMORY_CACHE_SWEEP_MS=60000 + + diff --git a/backend/src/lib/redis.ts b/backend/src/lib/redis.ts index 143bae5..ac85d54 100644 --- a/backend/src/lib/redis.ts +++ b/backend/src/lib/redis.ts @@ -85,7 +85,39 @@ class MemoryCache { } export const cache = new MemoryCache(); -setInterval(() => cache.cleanup(), 60000); + +let sweepInterval: ReturnType | undefined; + +/** + * Starts the memory cache cleanup sweep interval. + * Uses process.env.MEMORY_CACHE_SWEEP_MS (default 60,000ms) unless overridden. + */ +export function startMemoryCacheSweep(intervalMs?: number): void { + if (sweepInterval) { + clearInterval(sweepInterval); + } + + const configuredMs = Number.parseInt( + process.env.MEMORY_CACHE_SWEEP_MS ?? '60000', + 10 + ); + const ms = intervalMs ?? (Number.isFinite(configuredMs) ? configuredMs : 60000); + + sweepInterval = setInterval(() => cache.cleanup(), ms); +} + +/** + * Stops the active memory cache cleanup sweep interval to prevent timer leaks. + */ +export function stopMemoryCacheSweep(): void { + if (sweepInterval) { + clearInterval(sweepInterval); + sweepInterval = undefined; + } +} + +// Start memory cache sweep automatically on module load +startMemoryCacheSweep(); // --- Redis Pub/Sub Logic --- @@ -142,6 +174,7 @@ export async function connectRedis(): Promise { } export async function disconnectRedis(): Promise { + stopMemoryCacheSweep(); await Promise.all([_publisher?.quit(), _subscriber?.quit()]); _publisher = null; _subscriber = null; diff --git a/backend/tests/redis.test.ts b/backend/tests/redis.test.ts index 76f3510..63490a5 100644 --- a/backend/tests/redis.test.ts +++ b/backend/tests/redis.test.ts @@ -1,7 +1,16 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { cache, isRedisAvailable } from '../src/lib/redis.js'; +import { describe, it, expect, vi, afterAll } from 'vitest'; +import { + cache, + isRedisAvailable, + startMemoryCacheSweep, + stopMemoryCacheSweep, +} from '../src/lib/redis.js'; describe('Memory Cache', () => { + afterAll(() => { + stopMemoryCacheSweep(); + }); + it('should set and get values', () => { cache.set('key1', 'value1', 10); expect(cache.get('key1')).toBe('value1'); @@ -32,6 +41,56 @@ describe('Memory Cache', () => { }); }); +describe('Memory Cache Sweep Config', () => { + afterAll(() => { + stopMemoryCacheSweep(); + }); + + it('should allow starting and stopping sweep interval', () => { + vi.useFakeTimers(); + const cleanupSpy = vi.spyOn(cache, 'cleanup'); + + // Stop any existing sweep and start a fresh one with 1000ms interval + stopMemoryCacheSweep(); + startMemoryCacheSweep(1000); + + vi.advanceTimersByTime(2500); + expect(cleanupSpy).toHaveBeenCalledTimes(2); + + // Stop sweep and ensure no more calls happen + stopMemoryCacheSweep(); + vi.advanceTimersByTime(2000); + expect(cleanupSpy).toHaveBeenCalledTimes(2); + + cleanupSpy.mockRestore(); + vi.useRealTimers(); + }); + + it('should respect MEMORY_CACHE_SWEEP_MS env variable', () => { + vi.useFakeTimers(); + const cleanupSpy = vi.spyOn(cache, 'cleanup'); + + const originalEnv = process.env.MEMORY_CACHE_SWEEP_MS; + process.env.MEMORY_CACHE_SWEEP_MS = '500'; + + stopMemoryCacheSweep(); + startMemoryCacheSweep(); + + vi.advanceTimersByTime(1200); + expect(cleanupSpy).toHaveBeenCalledTimes(2); + + stopMemoryCacheSweep(); + + if (originalEnv === undefined) { + delete process.env.MEMORY_CACHE_SWEEP_MS; + } else { + process.env.MEMORY_CACHE_SWEEP_MS = originalEnv; + } + cleanupSpy.mockRestore(); + vi.useRealTimers(); + }); +}); + describe('Redis Available', () => { it('should return false if redis not initialized', () => { expect(isRedisAvailable()).toBe(false); diff --git a/package-lock.json b/package-lock.json index 5ebba31..060a2fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7798,6 +7798,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7859,6 +7860,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7880,6 +7882,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7901,6 +7904,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7922,6 +7926,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7963,6 +7968,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -7984,6 +7990,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8005,6 +8012,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8026,6 +8034,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" },