Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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


35 changes: 34 additions & 1 deletion backend/src/lib/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,39 @@ class MemoryCache {
}

export const cache = new MemoryCache();
setInterval(() => cache.cleanup(), 60000);

let sweepInterval: ReturnType<typeof setInterval> | 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 ---

Expand Down Expand Up @@ -142,6 +174,7 @@ export async function connectRedis(): Promise<void> {
}

export async function disconnectRedis(): Promise<void> {
stopMemoryCacheSweep();
await Promise.all([_publisher?.quit(), _subscriber?.quit()]);
_publisher = null;
_subscriber = null;
Expand Down
63 changes: 61 additions & 2 deletions backend/tests/redis.test.ts
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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);
Expand Down
9 changes: 9 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading