Skip to content

Replace O(n) deque scans with O(log n) ordering in ReActAgent's bounded slot cache - #4

Draft
java-dependency-upgrade-fixer[bot] wants to merge 1 commit into
evaluation/agentscope-pr-2432from
jaipilot/pr-3-GsevGGZgXGk3
Draft

Replace O(n) deque scans with O(log n) ordering in ReActAgent's bounded slot cache#4
java-dependency-upgrade-fixer[bot] wants to merge 1 commit into
evaluation/agentscope-pr-2432from
jaipilot/pr-3-GsevGGZgXGk3

Conversation

@java-dependency-upgrade-fixer

Copy link
Copy Markdown

What changed

ReActAgent's stateCache/permissionEngineCache bound (added in commit 73bb1cb, PR agentscope-ai#2432 mirror) tracked slot recency with a ConcurrentLinkedDeque<String>, using deque.remove(slot) to promote an accessed slot and deque.size() in a while loop to decide when to evict. Both operations are linear scans; with the cache sized up to 1,000 entries, every single call to activateSlotForContext/getAgentState paid up to ~1,000 String comparisons twice (once to promote, repeatedly to check size while trimming).

This change replaces the deque with:

  • ConcurrentHashMap<String, Long> slotLastAccess — each slot's most recent access-sequence number.
  • ConcurrentSkipListMap<Long, String> slotOrder — sequence number -> slot, giving an ordered, O(log n) structure for "oldest first" eviction.
  • AtomicLong slotAccessSequence — monotonic sequence generator.
  • AtomicInteger trackedSlotCount — O(1) bound check instead of scanning for size.

Promotion (recordSlotAccess) and eviction (trimCaches) are now O(log n) instead of O(n), where n is bounded at 1,000 (MAX_CACHED_SLOTS).

Correctness fix found during this pass

An initial version of this change decremented trackedSlotCount whenever trimCaches polled the globally-oldest sequence entry, without checking whether that entry was still the slot's authoritative position. Under concurrency, a slot being promoted by recordSlotAccess can transiently leave its old sequence entry in slotOrder a moment before it's cleaned up; if trimCaches polls that stale entry first, unconditionally evicting the slot's cache entries and decrementing the counter would (a) evict a slot that was just freshly accessed, and (b) permanently under-count trackedSlotCount, letting the cache silently grow past the 1,000-slot bound over time under sustained contention.

The fix makes the eviction conditional: slotLastAccess.remove(slot, oldest.getKey()) only succeeds (and only then do we remove from stateCache/permissionEngineCache and decrement the counter) when the polled sequence number is still that slot's current position. A stale entry is discarded from slotOrder (harmless — it was garbage) but does not shrink the tracked count, and the eviction loop continues until it finds a real, still-current oldest entry. This was verified empirically with a 32-thread, ~96k-operation stress test racing a small pool of "hot" slots against a stream of brand-new slots (maximizing promotion/eviction races); the 1,000-slot bound held exactly across repeated runs, both for stateCache and permissionEngineCache.

Preserved behavior

  • The 1,000-slot bound (MAX_CACHED_SLOTS) is unchanged.
  • LRU-style access ordering (a re-accessed slot is protected from eviction) is preserved.
  • Paired removal: eviction always removes a slot's AgentState and PermissionEngine together; a permission-engine entry can never outlive its paired state entry.
  • AgentStateStore reload behavior, legacy-state loading, and loader exception handling are untouched (no changes to loadOrCreateAgentStateForSlot).
  • Thread safety: all new fields are lock-free concurrent collections/atomics; no new locking was introduced.

Tests

Added ReActAgentSlotCacheEvictionTest (5 tests) locking: the 1,000-slot bound, LRU-refresh protection against eviction, paired state/permission-engine removal, concurrent-activation boundedness, and the no-store getAgentState bound. These pass unmodified against both the original PR-head implementation and the final candidate.

Limitations

  • The performance benefit is a deterministic, worst-case bookkeeping-cost reduction (~30x at cache saturation in a microbenchmark); it is negligible relative to real LLM round-trip latency in typical production traffic, but bounds CPU cost under very high call concurrency against a full cache.
  • ConcurrentSkipListMap.size() is also not O(1) per its Javadoc, which is why an explicit AtomicInteger counter is used instead of calling .size().

Generated by JAIPilot Cloud for #3 from Anthropic session sesn_01V7Z4eE6MSfGsevGGZgXGk3.

@skrcode

skrcode commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Accepted for the JAIPilot evidence campaign — additional success 7/10 after the ShardingSphere anchor.

Independent review:

  • Exact identity: the companion’s sole commit has parent 73bb1cb93c0b7c85f781f604dd0cef0efdf83030, the still-open upstream PR fix(#2261): add bounded eviction to stateCache and permissionEngineCache [FaaFyfxR9WAQrL7FcAgEHJvztd8cVMxvjHRS55rw1nwH] agentscope-ai/agentscope-java#2432 head. It targets that mirrored evaluation branch and remains a bot-authored draft.
  • Scope: two files only — ReActAgent.java and one directly relevant five-test cache contract suite.
  • Value: replaces saturated-cache ConcurrentLinkedDeque.remove(slot)/size() linear scans with O(log n) recency updates and an O(1) tracked-slot bound check; conditional authoritative-sequence removal protects a concurrently promoted slot from stale eviction.
  • Behavior proof: the identical five focused tests passed on the exact PR head and final candidate (5/5, zero failures), covering the 1,000-slot bound, refreshed-slot eviction order, paired state/permission eviction, concurrent activation, and no-store access.
  • Measurement: five comparable saturated-cache runs over 200k bookkeeping operations; baseline median 2448 ms, final candidate median 80–95 ms (about 25–30x in this synthetic bookkeeping fixture). This is not presented as end-to-end LLM latency.
  • Final gate: repository-native mvn clean verify for agentscope-core completed with BUILD SUCCESS; the original exact head also has successful Linux/Windows builds. The companion branch itself currently reports no independent GitHub checks.
  • Cost: Anthropic list cost $4.00; estimated total including sandbox runtime $4.04.

Boundary: this counts the proved cache-bookkeeping complexity reduction. It does not claim repository-wide serialization or that the companion repairs any pre-existing publish/trim ordering outside the measured changed path.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant