CAMEL-23267: Use a LinkedHashMap as inProgressRepository for file com…#22311
Conversation
gnodet
left a comment
There was a problem hiding this comment.
Claude Code on behalf of Guillaume Nodet
Overview
This PR addresses a real performance bug: SimpleLRUCache.remove() doesn't clean up its internal lastChanges deque, so after ~100K add/remove cycles the deque fills with ghost entries and compressChangesIfNeeded() iterates the entire deque on every put(), causing high CPU load. The fix replaces the default inProgressRepository backing store for the file component with a LinkedHashMap using insertion-order eviction.
Verdict: Request changes
Issues to address
-
Test assertion gap — see inline comment.
-
getMaxCacheSize()returns 0 — The new factory method doesn't callsetCacheSize(), sogetMaxCacheSize()(a JMX-exposed@ManagedAttribute) will return 0 for repositories created with this method. See inline comment.
Questions / discussion points
-
Root cause not fixed — The actual bug is in
SimpleLRUCache.remove()not cleaning uplastChanges. This PR works around it for one caller. Should we fixSimpleLRUCachedirectly instead? That would fix all callers without needing a new factory method. Other components (AWS2S3Endpoint,IBMCOSEndpoint) use the sameMemoryIdempotentRepository.memoryIdempotentRepository()pattern for their in-progress repositories and have the same issue. -
Thread safety note (pre-existing, non-blocking) —
getCacheSize()callscache.size()without acquiringcacheAndStoreLock, anddoStop()callscache.clear()without the lock. WithSimpleLRUCache(backed byConcurrentHashMap) this was benign, but with a plainLinkedHashMapthese become data races. Not introduced by this PR, but worth being aware of.
| } | ||
| for (int i = 0; i < cacheSize - entriesNotFittingInRepository; i++) { | ||
| assertFalse(repository.contains(String.valueOf(i)), "Repository should not contain entry " + i); | ||
| } |
There was a problem hiding this comment.
The loop condition i < cacheSize - entriesNotFittingInRepository equals i < 1, so this only checks that entry "0" was evicted. It should verify all 4 evicted entries (0, 1, 2, 3) are absent.
| } | |
| for (int i = 0; i < entriesNotFittingInRepository; i++) { |
| protected boolean removeEldestEntry(Entry<String, Object> eldest) { | ||
| return size() > cacheSize; | ||
| } | ||
| }; |
There was a problem hiding this comment.
This factory method passes the map to the MemoryIdempotentRepository(Map) constructor, which does not set the cacheSize field. As a result, getMaxCacheSize() (a JMX-exposed @ManagedAttribute) will return 0.
Consider setting it:
public static IdempotentRepository memoryIdempotentRepositoryInsertionOrder(int cacheSize) {
LinkedHashMap<String, Object> map = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(Entry<String, Object> eldest) {
return size() > cacheSize;
}
};
MemoryIdempotentRepository answer = new MemoryIdempotentRepository(map);
answer.setCacheSize(cacheSize);
return answer;
}|
Claude Code on behalf of Guillaume Nodet Follow-up: What about fixing
|
Fix SimpleLRUCache |
This PR's LinkedHashMap approach |
|
|---|---|---|
| Scope | Fixes all callers (file, AWS S3, IBM COS, any future use) | Only fixes the file component |
| Complexity | Small, targeted change in existing code | Adds a new factory method + new code path |
| LRU semantics | Preserves LRU eviction (access-order) | Switches to insertion-order eviction (FIFO) |
| Thread safety | Already thread-safe (ConcurrentHashMap + locks) |
Relies on MemoryIdempotentRepository's lock; getCacheSize()/doStop() are unguarded |
| Risk | Higher — SimpleLRUCache is used broadly, subtle concurrency bugs possible |
Lower — isolated change, only affects file component default |
| Performance | Slightly heavier (deque + ConcurrentHashMap overhead) | Lighter (plain LinkedHashMap, no deque) |
Recommendation
Ideally do both: fix SimpleLRUCache because the ghost entry bug is a real defect that will bite other callers (AWS S3 and IBM COS endpoints have the same in-progress repository pattern). But also keep the LinkedHashMap approach for the in-progress repository — it's simpler, lighter, and a better semantic fit since in-progress files don't need LRU reordering (they're added, processed, then removed). The SimpleLRUCache fix could be tracked as a separate JIRA ticket.
Description
To reduce cpu load when many files are processed, let's use a LinkedHashMap instead of SimpleLRUCache as the default inProgressRepository for the file component. See https://issues.apache.org/jira/browse/CAMEL-23267 and #camel > file consumer does not release items in SimpleLRUCache