Skip to content

Commit c41cf82

Browse files
committed
geez
1 parent ce86f16 commit c41cf82

2 files changed

Lines changed: 114 additions & 77 deletions

File tree

cache/index.js

Lines changed: 110 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -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++

cache/middleware.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,12 @@ const cacheQuery = async (req, res, next) => {
4141
const originalJson = res.json.bind(res)
4242

4343
res.json = (data) => {
44+
const workerId = process.env.pm_id || process.pid
4445
if (res.statusCode === 200 && Array.isArray(data)) {
46+
console.log(`[CACHE-MIDDLEWARE] Worker ${workerId}: Caching query result, key=${cacheKey.substring(0, 80)}...`)
4547
cache.set(cacheKey, data).catch(err => console.error('Cache set error:', err))
48+
} else {
49+
console.log(`[CACHE-MIDDLEWARE] Worker ${workerId}: NOT caching - status=${res.statusCode}, isArray=${Array.isArray(data)}`)
4650
}
4751
return originalJson(data)
4852
}

0 commit comments

Comments
 (0)