@@ -6,6 +6,10 @@ const activeRevalidations = new Map<string, Promise<void>>();
66
77const stringifyRegex = / ^ \d { 4 } - \d { 2 } - \d { 2 } T \d { 2 } : \d { 2 } : \d { 2 } .* Z $ / ;
88
9+ let redisAvailable = true ;
10+ let lastRedisCheck = 0 ;
11+ const REDIS_CHECK_INTERVAL = 30_000 ;
12+
913type CacheOptions = {
1014 expireInSec : number ;
1115 prefix ?: string ;
@@ -14,6 +18,7 @@ type CacheOptions = {
1418 staleWhileRevalidate ?: boolean ;
1519 staleTime ?: number ;
1620 maxRetries ?: number ;
21+ timeout ?: number ;
1722} ;
1823
1924const defaultSerialize = ( data : unknown ) : string => JSON . stringify ( data ) ;
@@ -25,6 +30,30 @@ const defaultDeserialize = (data: string): unknown =>
2530 return value ;
2631 } ) ;
2732
33+ function withTimeout < T > (
34+ promise : Promise < T > ,
35+ timeoutMs : number
36+ ) : Promise < T > {
37+ return Promise . race ( [
38+ promise ,
39+ new Promise < T > ( ( _ , reject ) =>
40+ setTimeout ( ( ) => reject ( new Error ( "Redis timeout" ) ) , timeoutMs )
41+ ) ,
42+ ] ) ;
43+ }
44+
45+ function shouldSkipRedis ( ) : boolean {
46+ const now = Date . now ( ) ;
47+ if ( ! redisAvailable && now - lastRedisCheck < REDIS_CHECK_INTERVAL ) {
48+ return true ;
49+ }
50+ if ( ! redisAvailable && now - lastRedisCheck >= REDIS_CHECK_INTERVAL ) {
51+ redisAvailable = true ;
52+ lastRedisCheck = now ;
53+ }
54+ return false ;
55+ }
56+
2857export async function getCache < T > (
2958 key : string ,
3059 options : CacheOptions | number ,
@@ -36,59 +65,92 @@ export async function getCache<T>(
3665 deserialize = defaultDeserialize ,
3766 staleWhileRevalidate = false ,
3867 staleTime = 0 ,
39- maxRetries = 3 ,
68+ maxRetries = 1 ,
69+ timeout = 300 ,
4070 } = typeof options === "number" ? { expireInSec : options } : options ;
4171
72+ if ( shouldSkipRedis ( ) ) {
73+ return fn ( ) ;
74+ }
75+
4276 let retries = 0 ;
4377 while ( retries < maxRetries ) {
4478 try {
4579 const redis = getRedisCache ( ) ;
46- const hit = await redis . get ( key ) ;
80+ const hit = await withTimeout ( redis . get ( key ) , timeout ) ;
81+ redisAvailable = true ;
82+ lastRedisCheck = Date . now ( ) ;
83+
4784 if ( hit ) {
4885 const data = deserialize ( hit ) as T ;
4986
5087 if ( staleWhileRevalidate ) {
51- const ttl = await redis . ttl ( key ) ;
52- if ( ttl < staleTime && ! activeRevalidations . has ( key ) ) {
53- // Return stale data and revalidate in background
54- const revalidationPromise = fn ( )
55- . then ( async ( freshData : T ) => {
56- if ( freshData !== undefined && freshData !== null ) {
57- const redis = getRedisCache ( ) ;
58- await redis . setex ( key , expireInSec , serialize ( freshData ) ) ;
59- }
60- } )
61- . catch ( ( error : unknown ) => {
62- logger . error (
63- `Background revalidation failed for key ${ key } :` ,
64- error
65- ) ;
66- } )
67- . finally ( ( ) => {
68- activeRevalidations . delete ( key ) ;
69- } ) ;
70- activeRevalidations . set ( key , revalidationPromise ) ;
88+ try {
89+ const ttl = await withTimeout ( redis . ttl ( key ) , timeout ) ;
90+ if ( ttl < staleTime && ! activeRevalidations . has ( key ) ) {
91+ const revalidationPromise = fn ( )
92+ . then ( async ( freshData : T ) => {
93+ if (
94+ freshData !== undefined &&
95+ freshData !== null &&
96+ redisAvailable
97+ ) {
98+ try {
99+ const redis = getRedisCache ( ) ;
100+ await withTimeout (
101+ redis . setex ( key , expireInSec , serialize ( freshData ) ) ,
102+ timeout
103+ ) ;
104+ } catch {
105+ // Ignore SET failure
106+ }
107+ }
108+ } )
109+ . catch ( ( error : unknown ) => {
110+ logger . error (
111+ `Background revalidation failed for key ${ key } :` ,
112+ error
113+ ) ;
114+ } )
115+ . finally ( ( ) => {
116+ activeRevalidations . delete ( key ) ;
117+ } ) ;
118+ activeRevalidations . set ( key , revalidationPromise ) ;
119+ }
120+ } catch {
121+ // Ignore TTL check failure
71122 }
72123 }
73124
74125 return data ;
75126 }
76127
77128 const data = await fn ( ) ;
78- if ( data !== undefined && data !== null ) {
79- await redis . setex ( key , expireInSec , serialize ( data ) ) ;
129+ if (
130+ data !== undefined &&
131+ data !== null &&
132+ redisAvailable &&
133+ ! shouldSkipRedis ( )
134+ ) {
135+ try {
136+ await withTimeout (
137+ redis . setex ( key , expireInSec , serialize ( data ) ) ,
138+ timeout
139+ ) ;
140+ } catch {
141+ redisAvailable = false ;
142+ lastRedisCheck = Date . now ( ) ;
143+ }
80144 }
81145 return data ;
82146 } catch ( error : unknown ) {
83147 retries += 1 ;
84- if ( retries === maxRetries ) {
85- logger . error (
86- `Cache error for key ${ key } after ${ maxRetries } retries:` ,
87- error
88- ) ;
148+ if ( retries >= maxRetries ) {
149+ redisAvailable = false ;
150+ lastRedisCheck = Date . now ( ) ;
151+ logger . error ( `Cache error for key ${ key } , skipping Redis:` , error ) ;
89152 return fn ( ) ;
90153 }
91- await new Promise ( ( resolve ) => setTimeout ( resolve , 100 * retries ) ) ; // Exponential backoff
92154 }
93155 }
94156
@@ -106,7 +168,6 @@ export function cacheable<T extends (...args: any) => any>(
106168 deserialize = defaultDeserialize ,
107169 staleWhileRevalidate = false ,
108170 staleTime = 0 ,
109- maxRetries = 3 ,
110171 } = typeof options === "number" ? { expireInSec : options } : options ;
111172
112173 const cachePrefix = `cacheable:${ prefix } ` ;
@@ -152,57 +213,94 @@ export function cacheable<T extends (...args: any) => any>(
152213 ...args : Parameters < T >
153214 ) : Promise < Awaited < ReturnType < T > > > => {
154215 const key = getKey ( ...args ) ;
155- let retries = 0 ;
216+ const timeout = typeof options === "number" ? 50 : options . timeout ?? 50 ;
217+ const retries = typeof options === "number" ? 1 : options . maxRetries ?? 1 ;
156218
157- while ( retries < maxRetries ) {
219+ if ( shouldSkipRedis ( ) ) {
220+ return fn ( ...args ) ;
221+ }
222+
223+ let attempt = 0 ;
224+ while ( attempt < retries ) {
158225 try {
159226 const redis = getRedisCache ( ) ;
160- const cached = await redis . get ( key ) ;
227+ const cached = await withTimeout ( redis . get ( key ) , timeout ) ;
228+ redisAvailable = true ;
229+ lastRedisCheck = Date . now ( ) ;
230+
161231 if ( cached ) {
162232 const data = deserialize ( cached ) as Awaited < ReturnType < T > > ;
163233
164234 if ( staleWhileRevalidate ) {
165- const ttl = await redis . ttl ( key ) ;
166- if ( ttl < staleTime && ! activeRevalidations . has ( key ) ) {
167- // Return stale data and revalidate in background
168- const revalidationPromise = fn ( ...args )
169- . then ( async ( freshData : Awaited < ReturnType < T > > ) => {
170- if ( freshData !== undefined && freshData !== null ) {
171- const redis = getRedisCache ( ) ;
172- await redis . setex ( key , expireInSec , serialize ( freshData ) ) ;
173- }
174- } )
175- . catch ( ( error : unknown ) => {
176- logger . error (
177- `Background revalidation failed for function ${ fn . name } :` ,
178- error
179- ) ;
180- } )
181- . finally ( ( ) => {
182- activeRevalidations . delete ( key ) ;
183- } ) ;
184- activeRevalidations . set ( key , revalidationPromise ) ;
235+ try {
236+ const ttl = await withTimeout ( redis . ttl ( key ) , timeout ) ;
237+ if ( ttl < staleTime && ! activeRevalidations . has ( key ) ) {
238+ const revalidationPromise = fn ( ...args )
239+ . then ( async ( freshData : Awaited < ReturnType < T > > ) => {
240+ if (
241+ freshData !== undefined &&
242+ freshData !== null &&
243+ redisAvailable
244+ ) {
245+ try {
246+ const redis = getRedisCache ( ) ;
247+ await withTimeout (
248+ redis . setex ( key , expireInSec , serialize ( freshData ) ) ,
249+ timeout
250+ ) ;
251+ } catch {
252+ // Ignore SET failure
253+ }
254+ }
255+ } )
256+ . catch ( ( error : unknown ) => {
257+ logger . error (
258+ `Background revalidation failed for function ${ fn . name } :` ,
259+ error
260+ ) ;
261+ } )
262+ . finally ( ( ) => {
263+ activeRevalidations . delete ( key ) ;
264+ } ) ;
265+ activeRevalidations . set ( key , revalidationPromise ) ;
266+ }
267+ } catch {
268+ // Ignore TTL check failure
185269 }
186270 }
187271
188272 return data ;
189273 }
190274
191275 const result = await fn ( ...args ) ;
192- if ( result !== undefined && result !== null ) {
193- await redis . setex ( key , expireInSec , serialize ( result ) ) ;
276+ if (
277+ result !== undefined &&
278+ result !== null &&
279+ redisAvailable &&
280+ ! shouldSkipRedis ( )
281+ ) {
282+ try {
283+ await withTimeout (
284+ redis . setex ( key , expireInSec , serialize ( result ) ) ,
285+ timeout
286+ ) ;
287+ } catch {
288+ redisAvailable = false ;
289+ lastRedisCheck = Date . now ( ) ;
290+ }
194291 }
195292 return result ;
196293 } catch ( error : unknown ) {
197- retries += 1 ;
198- if ( retries === maxRetries ) {
294+ attempt += 1 ;
295+ if ( attempt >= retries ) {
296+ redisAvailable = false ;
297+ lastRedisCheck = Date . now ( ) ;
199298 logger . error (
200- `Cache error for function ${ fn . name } after ${ maxRetries } retries :` ,
299+ `Cache error for function ${ fn . name } , skipping Redis :` ,
201300 error
202301 ) ;
203302 return fn ( ...args ) ;
204303 }
205- await new Promise ( ( resolve ) => setTimeout ( resolve , 100 * retries ) ) ; // Exponential backoff
206304 }
207305 }
208306
0 commit comments