Skip to content

Commit e5fa0f9

Browse files
authored
✨ tweak: optimize redis client and retry logic
1 parent 9cc05b2 commit e5fa0f9

File tree

2 files changed

+83
-6
lines changed

2 files changed

+83
-6
lines changed

src/config/redis.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ export const redisEventConfig = {
3636
keyPrefix: 'unthread:eventid:',
3737
};
3838

39-
// Create Redis client for v4.x - use URL string directly with timeout
39+
// Create Redis client for v4.x - Railway-optimized configuration
4040
const client = createClient({
4141
url: redisConfig.url,
4242
socket: {
43-
connectTimeout: 5000 // 5 second timeout
43+
connectTimeout: 10000, // 10 seconds for initial connection - Railway optimized
44+
keepAlive: 30000, // Keep connection alive (30 seconds)
45+
noDelay: true, // Disable Nagle's algorithm for better latency
4446
}
4547
});
4648

src/services/redisService.ts

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,12 @@ export class RedisService {
5151
completeTransformedData: event
5252
});
5353

54-
// Use Redis LIST for FIFO queue (LPUSH + BRPOP pattern)
55-
const result = await this.client.lPush(queueName, eventJson);
54+
// Use Redis LIST for FIFO queue with Railway-optimized retry logic
55+
const result = await this.executeWithRetry(
56+
() => this.client.lPush(queueName, eventJson),
57+
`lPush to ${queueName}`,
58+
3 // max retries
59+
);
5660

5761
LogEngine.info(`✅ Event successfully queued: ${event.data?.eventId || 'unknown'} -> ${queueName} (${result} items in queue)`);
5862
return result;
@@ -62,14 +66,81 @@ export class RedisService {
6266
}
6367
}
6468

69+
/**
70+
* Railway-optimized retry logic for Redis operations with timeout handling
71+
*/
72+
private async executeWithRetry<T>(
73+
operation: () => Promise<T>,
74+
operationName: string,
75+
maxRetries: number = 3,
76+
baseDelay: number = 1000,
77+
operationTimeout: number = 8000 // 8 seconds for individual operations
78+
): Promise<T> {
79+
let lastError: Error = new Error('Unknown error');
80+
81+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
82+
try {
83+
// Wrap operation with timeout for Railway optimization
84+
const timeoutPromise = new Promise<never>((_, reject) => {
85+
setTimeout(() => {
86+
reject(new Error(`Redis operation ${operationName} timed out after ${operationTimeout}ms`));
87+
}, operationTimeout);
88+
});
89+
90+
return await Promise.race([operation(), timeoutPromise]);
91+
} catch (error) {
92+
lastError = error as Error;
93+
94+
if (attempt === maxRetries) {
95+
LogEngine.error(`Redis operation ${operationName} failed after ${maxRetries} attempts: ${lastError.message}`);
96+
break;
97+
}
98+
99+
// Check if it's a timeout or connection error
100+
const isRetryableError = (
101+
lastError.message.includes('ETIMEDOUT') ||
102+
lastError.message.includes('ECONNRESET') ||
103+
lastError.message.includes('ENOTFOUND') ||
104+
lastError.message.includes('Connection is closed') ||
105+
lastError.message.includes('timed out')
106+
);
107+
108+
if (!isRetryableError) {
109+
LogEngine.error(`Redis operation ${operationName} failed with non-retryable error: ${lastError.message}`);
110+
break;
111+
}
112+
113+
const delay = baseDelay * Math.pow(2, attempt - 1); // Exponential backoff
114+
LogEngine.warn(`Redis operation ${operationName} failed (attempt ${attempt}/${maxRetries}), retrying in ${delay}ms: ${lastError.message}`);
115+
116+
await new Promise(resolve => setTimeout(resolve, delay));
117+
118+
// Try to reconnect if connection is closed
119+
if (!this.isConnected()) {
120+
try {
121+
await this.connect();
122+
} catch (reconnectError) {
123+
LogEngine.warn(`Failed to reconnect during retry: ${reconnectError}`);
124+
}
125+
}
126+
}
127+
}
128+
129+
throw lastError;
130+
}
131+
65132
/**
66133
* Check if webhook event already exists (duplicate detection)
67134
* @param eventId - Unique event identifier
68135
* @returns Promise<boolean> - true if event exists
69136
*/
70137
async eventExists(eventId: string): Promise<boolean> {
71138
const key = `${redisEventConfig.keyPrefix}${eventId}`;
72-
const exists = await this.client.exists(key);
139+
const exists = await this.executeWithRetry(
140+
() => this.client.exists(key),
141+
`exists check for ${eventId}`,
142+
2 // fewer retries for existence checks
143+
);
73144
return exists === 1;
74145
}
75146

@@ -81,7 +152,11 @@ export class RedisService {
81152
async markEventProcessed(eventId: string, ttlSeconds?: number): Promise<void> {
82153
const key = `${redisEventConfig.keyPrefix}${eventId}`;
83154
const ttl = ttlSeconds || redisEventConfig.eventTtl; // 3 days default
84-
await this.client.setEx(key, ttl, 'processed');
155+
await this.executeWithRetry(
156+
() => this.client.setEx(key, ttl, 'processed'),
157+
`setEx for ${eventId}`,
158+
2 // fewer retries for marking processed
159+
);
85160
}
86161

87162
async close(): Promise<void> {

0 commit comments

Comments
 (0)