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,
    };
  }
}