Spaces:
Sleeping
Sleeping
File size: 5,291 Bytes
05c5ed5 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 | import { Cache } from "./cache.interface";
import { RedisCache, type RedisCacheOptions } from "./redis-cache";
import { MemoryCache } from "./memory-cache";
import logger from "logger";
export interface SafeRedisCacheOptions extends RedisCacheOptions {
fallbackToMemory?: boolean;
serverCache?: Cache;
maxRetries?: number;
retryDelay?: number;
}
export class SafeRedisCache implements Cache {
private redisCache: RedisCache | null = null;
private serverCache: Cache;
private isRedisFailed = false;
private retryCount = 0;
private maxRetries: number;
private retryDelay: number;
private lastRetryTime = 0;
constructor(options: SafeRedisCacheOptions = {}) {
const {
fallbackToMemory = true,
serverCache,
maxRetries = 3,
retryDelay = 60000,
...redisOptions
} = options;
this.serverCache = serverCache || new MemoryCache();
this.maxRetries = maxRetries;
this.retryDelay = retryDelay;
if (fallbackToMemory) {
try {
this.redisCache = new RedisCache(redisOptions);
logger.info("SafeRedisCache: Redis initialized successfully");
} catch (error) {
logger.error(
"SafeRedisCache: Failed to initialize Redis, using memory cache",
error,
);
this.isRedisFailed = true;
}
} else {
this.redisCache = new RedisCache(redisOptions);
}
}
private async executeWithFallback<T>(
operation: () => Promise<T>,
fallbackOperation: () => Promise<T>,
operationName: string,
): Promise<T> {
if (this.isRedisFailed) {
// Check if we should retry Redis connection
const now = Date.now();
if (
this.retryCount < this.maxRetries &&
now - this.lastRetryTime > this.retryDelay
) {
this.lastRetryTime = now;
this.retryCount++;
logger.info(
`SafeRedisCache: Retrying Redis connection (attempt ${this.retryCount}/${this.maxRetries})`,
);
try {
// Test Redis connection with a simple operation
if (this.redisCache) {
await this.redisCache.has("__test__");
this.isRedisFailed = false;
this.retryCount = 0;
logger.info("SafeRedisCache: Redis connection restored");
}
} catch (error) {
logger.warn(`SafeRedisCache: Redis retry failed`, error);
}
}
}
if (!this.isRedisFailed && this.redisCache) {
try {
return await operation();
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
// Check for rate limit errors
if (
errorMessage.includes("rate limit") ||
errorMessage.includes("quota exceeded") ||
errorMessage.includes("too many requests") ||
errorMessage.includes("OOM") // Redis out of memory
) {
logger.warn(
`SafeRedisCache: Redis rate limit/quota exceeded for ${operationName}`,
error,
);
} else {
logger.error(
`SafeRedisCache: Redis operation failed for ${operationName}`,
error,
);
}
this.isRedisFailed = true;
return fallbackOperation();
}
}
return fallbackOperation();
}
async get<T>(key: string): Promise<T | undefined> {
return this.executeWithFallback(
() => this.redisCache!.get<T>(key),
() => this.serverCache.get<T>(key),
`get(${key})`,
);
}
async set(key: string, value: unknown, ttlMs?: number): Promise<void> {
return this.executeWithFallback(
async () => {
await this.redisCache!.set(key, value, ttlMs);
// Also set in memory cache as backup
await this.serverCache.set(key, value, ttlMs);
},
() => this.serverCache.set(key, value, ttlMs),
`set(${key})`,
);
}
async has(key: string): Promise<boolean> {
return this.executeWithFallback(
() => this.redisCache!.has(key),
() => this.serverCache.has(key),
`has(${key})`,
);
}
async delete(key: string): Promise<void> {
return this.executeWithFallback(
async () => {
await this.redisCache!.delete(key);
// Also delete from memory cache
await this.serverCache.delete(key);
},
() => this.serverCache.delete(key),
`delete(${key})`,
);
}
async clear(): Promise<void> {
return this.executeWithFallback(
async () => {
await this.redisCache!.clear();
// Also clear memory cache
await this.serverCache.clear();
},
() => this.serverCache.clear(),
"clear()",
);
}
async getAll(): Promise<Map<string, unknown>> {
return this.executeWithFallback(
() => this.redisCache!.getAll(),
() => this.serverCache.getAll(),
"getAll()",
);
}
async disconnect(): Promise<void> {
if (this.redisCache) {
await this.redisCache.disconnect();
}
}
isUsingRedis(): boolean {
return !this.isRedisFailed && this.redisCache !== null;
}
getCacheStatus(): { redis: boolean; retries: number; lastError?: string } {
return {
redis: this.isUsingRedis(),
retries: this.retryCount,
};
}
}
|