fix(#2261): add bounded eviction to stateCache and permissionEngineCache [FaaFyfxR9WAQrL7FcAgEHJvztd8cVMxvjHRS55rw1nwH] - #2432
Conversation
…ssionEngineCache [FaaFyfxR9WAQrL7FcAgEHJvztd8cVMxvjHRS55rw1nwH]
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
oss-maintainer
left a comment
There was a problem hiding this comment.
Code Review — Approved with Comments ✅
Overall: Good fix for the unbounded cache growth issue (#2261). The approach uses an LRU-style eviction with a ConcurrentLinkedDeque to track access order.
Analysis:
-
Problem:
stateCacheandpermissionEngineCacheareConcurrentHashMapinstances that grow unbounded in long-running singleton deployments, potentially causing memory leaks. -
Solution:
- Added
MAX_CACHED_SLOTS = 1000limit ConcurrentLinkedDeque<String> slotOrdertracks insertion/access orderrecordSlotAccesspromotes accessed slots to the tail (MRU position)trimCachesevicts oldest entries when size exceeds the limit
- Added
-
Correctness:
- ✅ Thread-safe: Uses
ConcurrentLinkedDequeandConcurrentHashMap - ✅ LRU semantics:
recordSlotAccessremoves and re-adds to promote to tail - ✅ Bounded memory: Cache size is capped at 1000 entries
- ✅ Thread-safe: Uses
Observations:
-
Performance:
slotOrder.remove(slot)is O(n) forConcurrentLinkedDeque. With 1000 entries, this could be slow ifrecordSlotAccessis called frequently (e.g., on every agent call). Consider using aLinkedHashMapwith access-order instead, or a dedicated LRU cache library. -
Race Condition: There's a window between
recordSlotAccessandtrimCacheswhere the cache can temporarily exceed the limit. This is probably acceptable for a cache, but worth noting. -
Eviction Granularity: The eviction removes from both
stateCacheandpermissionEngineCachesimultaneously. If a slot is evicted fromstateCachebut still inpermissionEngineCache(or vice versa), there could be inconsistency. However, since both caches are keyed by the same slot and updated together, this should be fine.
CI Status: ✅ All checks passed (Check License, Check Module Sync, build on ubuntu-latest, build on windows-latest, codecov/patch)
Verdict: The fix correctly addresses the memory leak issue. The LRU eviction strategy is sound, though the O(n) remove operation could be optimized in the future if performance becomes a concern.
Description
Fixes #2261
ReActAgent.stateCacheandpermissionEngineCacheare plainConcurrentHashMapinstances with no eviction mechanism. WhenHarnessAgentis used as a singleton serving many(userId, sessionId)slots (the recommended pattern per its Javadoc), both maps grow monotonically for the lifetime of the JVM, eventually causing OOM.Changes
MAX_CACHED_SLOTS = 1000— constant setting the maximum number of slot entries to retain in both caches.slotOrder— aConcurrentLinkedDeque<String>tracking slot insertion order (LRU, promoted on re-access).recordSlotAccess(String slot)— promotes a slot key to the most-recently-used position in the deque.trimCaches()— evicts the oldest entries from bothstateCacheandpermissionEngineCachewhen the slot count exceedsMAX_CACHED_SLOTS.activateSlotForContext()andgetAgentState()— every cache write path now callsrecordSlotAccess()andtrimCaches()to keep the cache bounded.Design decisions
java.util.concurrent.ConcurrentLinkedDeque(available since JDK 7). No Caffeine, Guava, or other external caching library.ConcurrentLinkedDequeis lock-free and thread-safe;ConcurrentHashMapoperations are safe by design.remove()inrecordSlotAccessis a no-op for new keys (the deque doesn't contain them yet).Testing
Cannot run
mvn testlocally (JDK 17 required, environment has JDK 8). The change is purely additive — 4 new private methods/fields + 6 insertions into existing code paths. No existing code was modified or removed.Wallet: FaaFyfxR9WAQrL7FcAgEHJvztd8cVMxvjHRS55rw1nwH