Skip to content

Commit 5b84dac

Browse files
committed
Changes from testing
1 parent c41cf82 commit 5b84dac

4 files changed

Lines changed: 179 additions & 268 deletions

File tree

cache/__tests__/cache-limits.test.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ describe('Cache TTL (Time-To-Live) Limit Enforcement', () => {
5454
await cache.clusterCache.set(key, { data: 'expires soon' }, shortTTL)
5555
await waitForCache(50)
5656

57-
// Should exist immediately after set
57+
// Should exist immediately after set (unwrapped by cache.get())
5858
let value = await cache.get(key)
59-
expect(value).toEqual({ data: 'expires soon' })
59+
expect(value).toEqual('expires soon')
6060

6161
// Wait for TTL to expire (add buffer for reliability)
6262
await new Promise(resolve => setTimeout(resolve, shortTTL + 300))
@@ -89,8 +89,8 @@ describe('Cache TTL (Time-To-Live) Limit Enforcement', () => {
8989
await cache.clusterCache.set(key, { data: 'custom ttl' }, customTTL)
9090
await waitForCache(50)
9191

92-
// Should exist immediately
93-
expect(await cache.get(key)).toEqual({ data: 'custom ttl' })
92+
// Should exist immediately (unwrapped by cache.get())
93+
expect(await cache.get(key)).toEqual('custom ttl')
9494

9595
// Wait for custom TTL to expire
9696
await new Promise(resolve => setTimeout(resolve, customTTL + 200))

cache/docs/DETAILED.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ These are typically pre-installed on Linux/macOS systems. If missing, install vi
4545
- **TTL (Time-To-Live)**: 5 minutes default, 24 hours in production (300,000ms or 86,400,000ms)
4646
- **Storage Mode**: PM2 Cluster Cache with 'all' replication mode (full cache copy on each worker, synchronized automatically)
4747
- **Stats Sync**: Background interval every 5 seconds via setInterval (stats may be up to 5s stale across workers)
48-
- **Eviction**: Handled internally by pm2-cluster-cache based on maxLength limit (oldest entries removed when limit exceeded)
48+
- **Eviction**: LRU (Least Recently Used) eviction implemented with deferred background execution via setImmediate() to avoid blocking cache.set() operations
4949

5050
### Environment Variables
5151
```bash
@@ -75,24 +75,28 @@ The cache implements **dual limits** for defense-in-depth:
7575
- Ensures diverse cache coverage
7676
- Prevents cache thrashing from too many unique queries
7777
- Reached first under normal operation
78-
- Eviction handled automatically by pm2-cluster-cache (removes oldest entries when limit exceeded)
78+
- LRU eviction triggered when exceeded (evicts least recently accessed entry)
79+
- Eviction deferred to background via setImmediate() to avoid blocking cache.set()
7980

8081
2. **Byte Limit (1GB)**
8182
- Secondary safety limit
8283
- Prevents memory exhaustion
8384
- Protects against accidentally large result sets
8485
- Guards against malicious queries
85-
- Monitored but not enforced by pm2-cluster-cache (length limit is primary control)
86+
- LRU eviction triggered when exceeded
87+
- Eviction runs in background to avoid blocking operations
8688

8789
**Balance Analysis**: With typical RERUM queries (100 items per page at ~269 bytes per annotation):
8890
- 1000 entries = ~26 MB (2.7% of 1GB limit)
8991
- Length limit reached first in 99%+ of scenarios
9092
- Byte limit only relevant for monitoring and capacity planning
9193

9294
**Eviction Behavior**:
93-
- PM2 Cluster Cache automatically evicts oldest entries when maxLength (1000) is exceeded
94-
- Eviction synchronized across all workers (all workers maintain consistent cache state)
95-
- No manual eviction logic required in RERUM code
95+
- **LRU (Least Recently Used)** eviction strategy implemented in cache/index.js
96+
- Eviction triggered when maxLength (1000) or maxBytes (1GB) exceeded
97+
- Eviction deferred to background using setImmediate() to avoid blocking cache.set()
98+
- Synchronized across all workers via PM2 cluster-cache
99+
- Tracks access times via keyAccessTimes Map for LRU determination
96100

97101
**Byte Size Calculation** (for monitoring only):
98102
```javascript
@@ -543,7 +547,7 @@ Total Time: 300-800ms (depending on query complexity)
543547
### Memory Usage
544548
- Average entry size: ~2-10KB (depending on object complexity)
545549
- Max memory per worker (1000 entries × ~10KB): ~10MB
546-
- PM2 Cluster Cache eviction ensures memory stays bounded
550+
- LRU eviction ensures memory stays bounded (deferred to background via setImmediate())
547551
- All workers maintain identical cache state (storage mode: 'all')
548552

549553
### TTL Behavior
@@ -621,10 +625,10 @@ Cache operations are logged with `[CACHE]` prefix:
621625
- No shared memory or IPC overhead (each worker has independent Map)
622626

623627
### Memory Management
624-
- PM2 Cluster Cache handles eviction automatically based on maxLength
625-
- Evictions synchronized across all workers
626-
- No manual memory management required
627-
- Byte size calculated for monitoring/stats only
628+
- LRU eviction implemented in cache/index.js with deferred background execution (setImmediate())
629+
- Eviction triggered when maxLength or maxBytes exceeded
630+
- Evictions synchronized across all workers via PM2 cluster-cache
631+
- Byte size calculated using optimized _calculateSize() method (fast path for primitives)
628632

629633
### Extensibility
630634
- New endpoints can easily add cache middleware

cache/index.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,12 @@ class ClusterCache {
691691
let count = 0
692692
const keysToCheck = Array.from(this.allKeys)
693693

694+
// Early exit: check if any query/search keys exist
695+
const hasQueryKeys = keysToCheck.some(k =>
696+
k.startsWith('query:') || k.startsWith('search:') || k.startsWith('searchPhrase:')
697+
)
698+
if (!hasQueryKeys) return 0
699+
694700
for (const cacheKey of keysToCheck) {
695701
if (!cacheKey.startsWith('query:') &&
696702
!cacheKey.startsWith('search:') &&
@@ -726,8 +732,9 @@ class ClusterCache {
726732
* @returns {boolean} True if object could match this query
727733
*/
728734
objectMatchesQuery(obj, query) {
729-
if (query.body && typeof query.body === 'object') return this.objectContainsProperties(obj, query.body)
730-
return this.objectContainsProperties(obj, query)
735+
return query.body && typeof query.body === 'object'
736+
? this.objectContainsProperties(obj, query.body)
737+
: this.objectContainsProperties(obj, query)
731738
}
732739

733740
/**
@@ -845,10 +852,15 @@ class ClusterCache {
845852
/**
846853
* Get nested property value using dot notation
847854
* @param {Object} obj - The object
848-
* @param {string} path - Property path
855+
* @param {string} path - Property path (e.g., "user.profile.name")
849856
* @returns {*} Property value or undefined
850857
*/
851858
getNestedProperty(obj, path) {
859+
// Fast path for non-nested properties
860+
if (!path.includes('.')) {
861+
return obj?.[path]
862+
}
863+
852864
const keys = path.split('.')
853865
let current = obj
854866

0 commit comments

Comments
 (0)