@@ -80,24 +80,29 @@ class ClusterCache {
8080 */
8181 async get ( key ) {
8282 try {
83- const value = await this . clusterCache . get ( key , undefined )
84- if ( value !== undefined ) {
83+ const wrappedValue = await this . clusterCache . get ( key , undefined )
84+ if ( wrappedValue !== undefined ) {
8585 this . stats . hits ++
8686 this . keyAccessTimes . set ( key , Date . now ( ) ) // Update access time for LRU
87- return value
87+ // Unwrap the value if it's wrapped with metadata
88+ return wrappedValue . data !== undefined ? wrappedValue . data : wrappedValue
8889 }
89- if ( this . localCache . has ( key ) ) {
90+ // Check local cache (single lookup instead of has + get)
91+ const localValue = this . localCache . get ( key )
92+ if ( localValue !== undefined ) {
9093 this . stats . hits ++
9194 this . keyAccessTimes . set ( key , Date . now ( ) ) // Update access time for LRU
92- return this . localCache . get ( key )
95+ return localValue
9396 }
9497 this . stats . misses ++
9598 return null
9699 } catch ( err ) {
97- if ( this . localCache . has ( key ) ) {
100+ // Fallback to local cache on error (single lookup)
101+ const localValue = this . localCache . get ( key )
102+ if ( localValue !== undefined ) {
98103 this . stats . hits ++
99104 this . keyAccessTimes . set ( key , Date . now ( ) ) // Update access time for LRU
100- return this . localCache . get ( key )
105+ return localValue
101106 }
102107 this . stats . misses ++
103108 return null
@@ -106,14 +111,32 @@ class ClusterCache {
106111
107112 /**
108113 * Calculate approximate size of a value in bytes
114+ * Fast estimation - avoids JSON.stringify for simple types
109115 * @param {* } value - Value to measure
110116 * @returns {number } Approximate size in bytes
111117 * @private
112118 */
113119 _calculateSize ( value ) {
114120 if ( value === null || value === undefined ) return 0
121+
122+ // Fast path for primitives
123+ const type = typeof value
124+ if ( type === 'string' ) return value . length * 2
125+ if ( type === 'number' ) return 8
126+ if ( type === 'boolean' ) return 4
127+
128+ // For arrays with simple values, estimate quickly
129+ if ( Array . isArray ( value ) ) {
130+ if ( value . length === 0 ) return 8
131+ // If small array, just estimate
132+ if ( value . length < 10 ) {
133+ return value . reduce ( ( sum , item ) => sum + this . _calculateSize ( item ) , 16 )
134+ }
135+ }
136+
137+ // For objects/complex types, fall back to JSON stringify
138+ // This is still expensive but only for complex objects
115139 const str = JSON . stringify ( value )
116- // Each character is approximately 2 bytes in UTF-16
117140 return str . length * 2
118141 }
119142
@@ -124,68 +147,69 @@ class ClusterCache {
124147 */
125148 async set ( key , value ) {
126149 try {
127- const valueSize = this . _calculateSize ( value )
150+ const now = Date . now ( )
128151 const isUpdate = this . allKeys . has ( key )
129152
153+ // Calculate size only once (can be expensive for large objects)
154+ const valueSize = this . _calculateSize ( value )
155+
130156 // If updating existing key, subtract old size first
131157 if ( isUpdate ) {
132158 const oldSize = this . keySizes . get ( key ) || 0
133159 this . totalBytes -= oldSize
134160 }
135161
136- // Get cluster-wide metrics for accurate limit enforcement
137- const clusterKeyCount = await this . _getClusterKeyCount ( )
138-
139- // Check if we need to evict due to maxLength (cluster-wide)
140- if ( clusterKeyCount >= this . maxLength && ! isUpdate ) {
141- await this . _evictLRU ( )
162+ // Wrap value with metadata to prevent PM2 cluster-cache deduplication
163+ const wrappedValue = {
164+ data : value ,
165+ key : key ,
166+ cachedAt : now ,
167+ size : valueSize
142168 }
143169
144- // Check if we need to evict due to maxBytes (cluster-wide)
145- let clusterTotalBytes = await this . _getClusterTotalBytes ( )
146- let evictionCount = 0
147- const maxEvictions = 100 // Prevent infinite loops
148-
149- while ( clusterTotalBytes + valueSize > this . maxBytes &&
150- this . allKeys . size > 0 &&
151- evictionCount < maxEvictions ) {
152- await this . _evictLRU ( )
153- evictionCount ++
154- // Recalculate cluster total bytes after eviction
155- clusterTotalBytes = await this . _getClusterTotalBytes ( )
156- }
170+ // Set in cluster cache immediately (most critical operation)
171+ await this . clusterCache . set ( key , wrappedValue , this . ttl )
157172
158- await this . clusterCache . set ( key , value , this . ttl )
173+ // Update local state (reuse precalculated values )
159174 this . stats . sets ++
160175 this . allKeys . add ( key )
161- this . keyAccessTimes . set ( key , Date . now ( ) ) // Track access time
162- this . keySizes . set ( key , valueSize ) // Track size
176+ this . keyAccessTimes . set ( key , now )
177+ this . keySizes . set ( key , valueSize )
163178 this . totalBytes += valueSize
164179 this . localCache . set ( key , value )
180+
181+ // Check limits and evict if needed (do this after set to avoid blocking)
182+ // Use setImmediate to defer eviction checks without blocking
183+ setImmediate ( async ( ) => {
184+ try {
185+ const clusterKeyCount = await this . _getClusterKeyCount ( )
186+ if ( clusterKeyCount > this . maxLength ) {
187+ await this . _evictLRU ( )
188+ }
189+
190+ let clusterTotalBytes = await this . _getClusterTotalBytes ( )
191+ let evictionCount = 0
192+ const maxEvictions = 100
193+
194+ while ( clusterTotalBytes > this . maxBytes &&
195+ this . allKeys . size > 0 &&
196+ evictionCount < maxEvictions ) {
197+ await this . _evictLRU ( )
198+ evictionCount ++
199+ clusterTotalBytes = await this . _getClusterTotalBytes ( )
200+ }
201+ } catch ( err ) {
202+ console . error ( 'Background eviction error:' , err )
203+ }
204+ } )
165205 } catch ( err ) {
166206 console . error ( 'Cache set error:' , err )
167- // Fallback: still enforce eviction on local cache
207+ // Fallback: still update local cache
168208 const valueSize = this . _calculateSize ( value )
169- const isUpdate = this . allKeys . has ( key )
170-
171- if ( isUpdate ) {
172- const oldSize = this . keySizes . get ( key ) || 0
173- this . totalBytes -= oldSize
174- }
175-
176- if ( this . allKeys . size >= this . maxLength && ! isUpdate ) {
177- await this . _evictLRU ( )
178- }
179-
180- while ( this . totalBytes + valueSize > this . maxBytes && this . allKeys . size > 0 ) {
181- await this . _evictLRU ( )
182- }
183-
184209 this . localCache . set ( key , value )
185210 this . allKeys . add ( key )
186211 this . keyAccessTimes . set ( key , Date . now ( ) )
187212 this . keySizes . set ( key , valueSize )
188- this . totalBytes += valueSize
189213 this . stats . sets ++
190214 }
191215 }
@@ -220,22 +244,45 @@ class ClusterCache {
220244 */
221245 /**
222246 * Clear all cache entries and reset stats across all workers
247+ *
248+ * Note: This clears immediately but stats sync happens every 5 seconds.
249+ * Wait 6+ seconds after calling clear() before checking /cache/stats for accurate results.
223250 */
224251 async clear ( ) {
225252 try {
226253 clearInterval ( this . statsInterval )
227254
228255 // Increment clear generation to signal all workers
229256 this . clearGeneration ++
257+ const clearGen = this . clearGeneration
258+
259+ // Flush all cache data FIRST
260+ await this . clusterCache . flush ( )
230261
231- // Broadcast clear signal to all workers via cluster cache
262+ // THEN set the clear signal AFTER flush so it doesn't get deleted
263+ // This allows other workers to see the signal and clear their local state
232264 await this . clusterCache . set ( '_clear_signal' , {
233- generation : this . clearGeneration ,
265+ generation : clearGen ,
234266 timestamp : Date . now ( )
235267 } , 60000 ) // 1 minute TTL
236268
237- // Flush all cache data
238- await this . clusterCache . flush ( )
269+ // Delete all old worker stats keys immediately
270+ try {
271+ const keysMap = await this . clusterCache . keys ( )
272+ const deletePromises = [ ]
273+ for ( const instanceKeys of Object . values ( keysMap ) ) {
274+ if ( Array . isArray ( instanceKeys ) ) {
275+ for ( const key of instanceKeys ) {
276+ if ( key . startsWith ( '_stats_worker_' ) ) {
277+ deletePromises . push ( this . clusterCache . delete ( key ) )
278+ }
279+ }
280+ }
281+ }
282+ await Promise . all ( deletePromises )
283+ } catch ( err ) {
284+ console . error ( 'Error deleting worker stats:' , err )
285+ }
239286
240287 // Reset local state
241288 this . allKeys . clear ( )
@@ -260,27 +307,6 @@ class ClusterCache {
260307
261308 // Immediately sync our fresh stats
262309 await this . _syncStats ( )
263-
264- // Wait for all workers to see the clear signal and reset
265- // Workers check every 5 seconds, so wait 6 seconds to be safe
266- await new Promise ( resolve => setTimeout ( resolve , 6000 ) )
267-
268- // Delete all old worker stats keys
269- const keysMap = await this . clusterCache . keys ( )
270- const deletePromises = [ ]
271- for ( const instanceKeys of Object . values ( keysMap ) ) {
272- if ( Array . isArray ( instanceKeys ) ) {
273- for ( const key of instanceKeys ) {
274- if ( key . startsWith ( '_stats_worker_' ) ) {
275- deletePromises . push ( this . clusterCache . delete ( key ) )
276- }
277- }
278- }
279- }
280- await Promise . all ( deletePromises )
281-
282- // Final sync after cleanup
283- await this . _syncStats ( )
284310 } catch ( err ) {
285311 console . error ( 'Cache clear error:' , err )
286312 this . localCache . clear ( )
@@ -319,7 +345,8 @@ class ClusterCache {
319345 for ( const instanceKeys of Object . values ( keysMap ) ) {
320346 if ( Array . isArray ( instanceKeys ) ) {
321347 instanceKeys . forEach ( key => {
322- if ( ! key . startsWith ( '_stats_worker_' ) ) {
348+ // Exclude internal keys from count
349+ if ( ! key . startsWith ( '_stats_worker_' ) && key !== '_clear_signal' ) {
323350 uniqueKeys . add ( key )
324351 }
325352 } )
@@ -425,7 +452,8 @@ class ClusterCache {
425452 for ( const instanceKeys of Object . values ( keysMap ) ) {
426453 if ( Array . isArray ( instanceKeys ) ) {
427454 instanceKeys . forEach ( key => {
428- if ( ! key . startsWith ( '_stats_worker_' ) ) {
455+ // Exclude internal keys from cache length
456+ if ( ! key . startsWith ( '_stats_worker_' ) && key !== '_clear_signal' ) {
429457 uniqueKeys . add ( key )
430458 }
431459 } )
@@ -497,12 +525,17 @@ class ClusterCache {
497525 const details = [ ]
498526 let position = 0
499527 for ( const key of allKeys ) {
500- const value = await this . clusterCache . get ( key , undefined )
501- const size = this . _calculateSize ( value )
528+ const wrappedValue = await this . clusterCache . get ( key , undefined )
529+ // Handle both wrapped and unwrapped values
530+ const actualValue = wrappedValue ?. data !== undefined ? wrappedValue . data : wrappedValue
531+ const size = wrappedValue ?. size || this . _calculateSize ( actualValue )
532+ const cachedAt = wrappedValue ?. cachedAt || Date . now ( )
533+ const age = Date . now ( ) - cachedAt
502534
503535 details . push ( {
504536 position,
505537 key,
538+ age : this . _formatUptime ( age ) ,
506539 bytes : size
507540 } )
508541 position ++
0 commit comments