@@ -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