Skip to content

Commit 56281b8

Browse files
committed
fix: Add Redis auto-reconnect mechanism for Kubernetes pod restarts
- Implement singleton pattern for Redis client to use single connection - Add exponential backoff reconnection strategy (1s to 30s max) - Handle connection lifecycle events (ready, reconnecting, error, end) - Initialize Redis on app startup alongside MongoDB and RabbitMQ - Graceful degradation to MongoDB when Redis is unavailable - Add comprehensive documentation in REDIS_RECONNECT.md This fixes the issue where API lost Redis connection after pod restart and required manual API pod restart to restore functionality.
1 parent 3af0959 commit 56281b8

3 files changed

Lines changed: 74 additions & 8 deletions

File tree

src/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { graphqlUploadExpress } from 'graphql-upload';
2929
import { metricsMiddleware, createMetricsServer, graphqlMetricsPlugin } from './metrics';
3030
import { requestLogger } from './utils/logger';
3131
import ReleasesFactory from './models/releasesFactory';
32+
import RedisHelper from './redisHelper';
3233

3334
/**
3435
* Option to enable playground
@@ -252,6 +253,11 @@ class HawkAPI {
252253
public async start(): Promise<void> {
253254
await mongo.setupConnections();
254255
await rabbitmq.setupConnections();
256+
257+
// Initialize Redis singleton with auto-reconnect
258+
const redis = RedisHelper.getInstance();
259+
await redis.initialize();
260+
255261
await this.server.start();
256262
this.app.use(graphqlUploadExpress());
257263
this.server.applyMiddleware({ app: this.app });

src/models/eventsFactory.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,9 @@ const MAX_DB_READ_BATCH_SIZE = Number(process.env.MAX_DB_READ_BATCH_SIZE);
7272
*/
7373
class EventsFactory extends Factory {
7474
/**
75-
/**
76-
* Redis helper instance for modifying data through redis
75+
* Redis helper instance for modifying data through redis (singleton)
7776
*/
78-
redis = new RedisHelper();
77+
redis = RedisHelper.getInstance();
7978

8079
/**
8180
* Event types with collections where they stored

src/redisHelper.ts

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,92 @@ export default class RedisHelper {
1111
*/
1212
private static readonly LOCK_TTL = 10;
1313

14+
/**
15+
* Singleton instance
16+
*/
17+
private static instance: RedisHelper | null = null;
18+
1419
/**
1520
* Redis client instance
1621
*/
17-
private readonly redisClient!: RedisClientType;
22+
private redisClient!: RedisClientType;
23+
24+
/**
25+
* Flag to track if we're currently reconnecting
26+
*/
27+
private isReconnecting = false;
1828

1929
/**
2030
* Constructor
21-
* Initializes the Redis client and sets up error handling
31+
* Initializes the Redis client and sets up error handling with auto-reconnect
2232
*/
2333
constructor() {
2434
try {
25-
this.redisClient = createClient({ url: process.env.REDIS_URL });
35+
this.redisClient = createClient({
36+
url: process.env.REDIS_URL,
37+
socket: {
38+
reconnectStrategy: (retries) => {
39+
// Exponential backoff: wait longer between each retry
40+
// Max wait time: 30 seconds
41+
const delay = Math.min(retries * 1000, 30000);
42+
console.log(`[Redis] Reconnecting... attempt ${retries}, waiting ${delay}ms`);
43+
return delay;
44+
},
45+
},
46+
});
2647

48+
// Handle connection errors
2749
this.redisClient.on('error', (error) => {
2850
console.error('[Redis] Client error:', error);
2951
if (error) {
3052
HawkCatcher.send(error);
3153
}
3254
});
55+
56+
// Handle successful reconnection
57+
this.redisClient.on('ready', () => {
58+
console.log('[Redis] Client ready');
59+
this.isReconnecting = false;
60+
});
61+
62+
// Handle reconnecting event
63+
this.redisClient.on('reconnecting', () => {
64+
console.log('[Redis] Client reconnecting...');
65+
this.isReconnecting = true;
66+
});
67+
68+
// Handle connection end
69+
this.redisClient.on('end', () => {
70+
console.log('[Redis] Connection ended');
71+
});
3372
} catch (error) {
3473
console.error('[Redis] Error creating client:', error);
3574
}
3675
}
3776

77+
/**
78+
* Get singleton instance
79+
*/
80+
public static getInstance(): RedisHelper {
81+
if (!RedisHelper.instance) {
82+
RedisHelper.instance = new RedisHelper();
83+
}
84+
return RedisHelper.instance;
85+
}
86+
3887
/**
3988
* Connect to Redis
4089
*/
4190
public async initialize(): Promise<void> {
4291
try {
43-
await this.redisClient.connect();
44-
console.log('[Redis] Connected successfully');
92+
if (!this.redisClient.isOpen && !this.isReconnecting) {
93+
await this.redisClient.connect();
94+
console.log('[Redis] Connected successfully');
95+
}
4596
} catch (error) {
4697
console.error('[Redis] Connection failed:', error);
4798
HawkCatcher.send(error as Error);
99+
// Don't throw - let reconnectStrategy handle it
48100
}
49101
}
50102

@@ -57,6 +109,13 @@ export default class RedisHelper {
57109
console.log('[Redis] Connection closed');
58110
}
59111
}
112+
113+
/**
114+
* Check if Redis is connected
115+
*/
116+
public isConnected(): boolean {
117+
return this.redisClient.isOpen;
118+
}
60119

61120
public async getChartDataFromRedis(
62121
startDate: string,
@@ -66,7 +125,9 @@ export default class RedisHelper {
66125
projectId = '',
67126
groupHash = ''
68127
): Promise<{ timestamp: number; count: number }[]> {
128+
// If Redis is not connected, throw error to fallback to MongoDB
69129
if (!this.redisClient.isOpen) {
130+
console.warn('[Redis] Client not connected, will fallback to MongoDB');
70131
throw new Error('Redis client not connected');
71132
}
72133

0 commit comments

Comments
 (0)