From a30cfffddca5db478cd4e351ed74ec9287dbdd4d Mon Sep 17 00:00:00 2001 From: Narendranath Gogineni Date: Fri, 10 Oct 2025 18:30:20 +0530 Subject: [PATCH] extend redis client backend to support redis specific functionality --- src/shared/services/cache/backends/redis.ts | 38 ++++++++----------- src/shared/services/cache/index.ts | 4 ++ .../services/cache/utils/rateLimiter.ts | 16 ++++---- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/shared/services/cache/backends/redis.ts b/src/shared/services/cache/backends/redis.ts index 732104b7..64bd4db5 100644 --- a/src/shared/services/cache/backends/redis.ts +++ b/src/shared/services/cache/backends/redis.ts @@ -6,6 +6,8 @@ import Redis from 'ioredis'; import { CacheBackend, CacheEntry, CacheOptions, CacheStats } from '../types'; +type RedisClient = Redis; + // Using console.log for now to avoid build issues const logger = { debug: (msg: string, ...args: any[]) => @@ -18,22 +20,6 @@ const logger = { console.error(`[RedisCache] ${msg}`, ...args), }; -// Redis client interface matching ioredis -interface RedisClient { - get(key: string): Promise; - set( - key: string, - value: string, - expiryMode?: string | any, - time?: number | string - ): Promise<'OK' | null>; - del(...keys: string[]): Promise; - exists(...keys: string[]): Promise; - keys(pattern: string): Promise; - flushdb(): Promise<'OK'>; - quit(): Promise<'OK'>; -} - export class RedisCacheBackend implements CacheBackend { private client: RedisClient; private dbName: string; @@ -52,12 +38,6 @@ export class RedisCacheBackend implements CacheBackend { this.dbName = dbName; } - private getFullKey(key: string, namespace?: string): string { - return namespace - ? `${this.dbName}:${namespace}:${key}` - : `${this.dbName}:default:${key}`; - } - private serializeEntry(entry: CacheEntry): string { return JSON.stringify(entry); } @@ -70,6 +50,12 @@ export class RedisCacheBackend implements CacheBackend { return entry.expiresAt !== undefined && entry.expiresAt <= Date.now(); } + getFullKey(key: string, namespace?: string): string { + return namespace + ? `${this.dbName}:${namespace}:${key}` + : `${this.dbName}:default:${key}`; + } + async get( key: string, namespace?: string @@ -216,6 +202,14 @@ export class RedisCacheBackend implements CacheBackend { } } + async script(mode: 'LOAD' | 'EXISTS', script: string): Promise { + return await this.client.script('LOAD', script); + } + + async evalsha(sha: string, keys: string[], args: string[]): Promise { + return await this.client.evalsha(sha, keys.length, ...keys, ...args); + } + async cleanup(): Promise { // Redis handles TTL automatically, so this is mostly a no-op // We could scan for entries with manual expiration and clean them up diff --git a/src/shared/services/cache/index.ts b/src/shared/services/cache/index.ts index a229d2b8..8a5941ca 100644 --- a/src/shared/services/cache/index.ts +++ b/src/shared/services/cache/index.ts @@ -253,6 +253,10 @@ export class CacheService { })); return Promise.all(promises); } + + getClient(): CacheBackend { + return this.backend; + } } // Default cache instances for different use cases diff --git a/src/shared/services/cache/utils/rateLimiter.ts b/src/shared/services/cache/utils/rateLimiter.ts index bba149cd..2b478f01 100644 --- a/src/shared/services/cache/utils/rateLimiter.ts +++ b/src/shared/services/cache/utils/rateLimiter.ts @@ -1,5 +1,6 @@ import { Redis, Cluster } from 'ioredis'; import { RateLimiterKeyTypes } from '../../../../globals'; +import { RedisCacheBackend } from '../backends/redis'; const RATE_LIMIT_LUA = ` local tokensKey = KEYS[1] @@ -81,7 +82,7 @@ return {allowed, waitTime, currentTokens} `; class RedisRateLimiter { - private redis: Redis | Cluster; + private redis: RedisCacheBackend; private capacity: number; private windowSize: number; private tokensKey: string; @@ -92,7 +93,7 @@ class RedisRateLimiter { private key: string; constructor( - redisClient: Redis | Cluster, + redisClient: RedisCacheBackend, capacity: number, windowSize: number, key: string, @@ -103,8 +104,8 @@ class RedisRateLimiter { const tag = `{rate:${key}}`; // ensures same hash slot this.capacity = capacity; this.windowSize = windowSize; - this.tokensKey = `${tag}:tokens`; - this.lastRefillKey = `${tag}:lastRefill`; + this.tokensKey = `default:default:${tag}:tokens`; + this.lastRefillKey = `default:default:${tag}:lastRefill`; this.keyTTL = windowSize * ttlFactor; // dynamic TTL this.keyType = keyType; this.key = key; @@ -126,12 +127,12 @@ class RedisRateLimiter { const sha = await this.loadOrGetScriptSha(); try { - return await this.redis.evalsha(sha, keys.length, ...keys, ...args); + return await this.redis.evalsha(sha, keys, args); } catch (error: any) { if (error.message.includes('NOSCRIPT')) { // Script not loaded on target node - load it and retry with same SHA await this.redis.script('LOAD', RATE_LIMIT_LUA); - return await this.redis.evalsha(sha, keys.length, ...keys, ...args); + return await this.redis.evalsha(sha, keys, args); } throw error; } @@ -171,7 +172,8 @@ class RedisRateLimiter { } async getToken(): Promise { - return this.redis.get(this.tokensKey); + const cacheEntry = await this.redis.get(this.tokensKey); + return cacheEntry ? cacheEntry.value : null; } async decrementToken(