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.
AgentScopeJavaBot
left a comment
There was a problem hiding this comment.
🤖 AI Review
This PR adds an LRU-style cache eviction mechanism to ReActAgent's slot caches, capping at 1000 entries. The intent is valid but implementation has a critical JDK contract violation: trimCaches() is called inside computeIfAbsent(), which modifies the map during computation. Note: PR title ("fix file path resolution on different OS") is completely unrelated.
(inline comments could not be attached — line numbers fell outside PR hunks. See archived report.)
AgentScopeJavaBot
left a comment
There was a problem hiding this comment.
🤖 AI Review
This PR adds an LRU-style cache eviction mechanism to ReActAgent's slot caches, capping at 1000 entries. The intent is valid but implementation has a critical JDK contract violation: trimCaches() is called inside computeIfAbsent(), which modifies the map during computation. Note: PR title ("fix file path resolution on different OS") is completely unrelated.
(inline comments could not be attached — line numbers fell outside PR hunks. See archived report.)
AgentScopeJavaBot
left a comment
There was a problem hiding this comment.
🤖 AI Review
This PR adds an LRU-style cache eviction mechanism to ReActAgent's slot caches, capping at 1000 entries. The intent is valid but implementation has a critical JDK contract violation: trimCaches() is called inside computeIfAbsent(), which modifies the map during computation. Note: PR title ("fix file path resolution on different OS") is completely unrelated.
(inline comments could not be attached — line numbers fell outside PR hunks. See archived report.)
|
I ran JAIPilot Cloud against this exact PR head. It found that the new bounded caches still use linear ConcurrentLinkedDeque.remove and size scans on each slot access. The follow-up uses an indexed recency order and an explicit count, making promotion and oldest selection O(log n) and the bound check O(1). Five behavior tests preserve the 1,000-slot bound, LRU refresh, paired cache removal, concurrent activation, and no-store access. Across five comparable saturated-cache runs over 200,000 bookkeeping operations, the exact head had a 2,448 ms median versus 80 to 95 ms for the candidate; agentscope-core mvn clean verify passed. PR directly onto this source branch: waterWang#1 This is a synthetic cache-bookkeeping result, not end-to-end LLM latency. It preserves the existing cache lifecycle and does not claim to fix any independent publish-versus-trim ordering window. |
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