shashank created CAMEL-26740:
--------------------------------

             Summary: camel-kafka - KafkaKeyValueRepository keys() and size() 
fail once an entry has expired, and putIfAbsent can report an insert that did 
not happen
                 Key: CAMEL-26740
                 URL: https://issues.apache.org/jira/browse/CAMEL-26740
             Project: Camel
          Issue Type: Bug
          Components: camel-kafka
            Reporter: shashank
            Assignee: shashank


Two problems with the expiry handling of {{KafkaKeyValueRepository}} 
(camel-kafka, since 4.23):

*1. keys() and size() throw UnsupportedOperationException once an entry has 
expired*

{{keys()}} and {{size()}} (also the JMX attribute {{Size}}) first call 
{{evictExpired()}}, which removes expired entries with {{Iterator.remove()}} on 
{{cache.entrySet().iterator()}}. The cache comes from {{LRUCacheFactory}}, by 
default a {{SimpleLRUCache}}, whose {{entrySet()}} is an unmodifiable copy 
({{Collectors.toUnmodifiableSet()}}). So as soon as one entry with a time to 
live has expired, every call of {{keys()}} and {{size()}} fails with 
{{UnsupportedOperationException: remove}}, and so do the callers of {{keys()}} 
(for example the {{keys}} and {{size}} operations of the state-store 
component). The class Javadoc says that entries replayed from the topic on 
startup "will appear as expired immediately", so after a restart this can 
happen at once.

{code:java}
repo.put("a", "1", Duration.ofSeconds(1));
// one second later
repo.keys(); // UnsupportedOperationException: remove
{code}

*2. putIfAbsent can return null ("stored") although nothing was stored or sent*

{{putIfAbsent}} reads the entry, removes it if it has expired, then calls 
{{cache.putIfAbsent}}. When another writer stored an entry for the key in 
between (another thread, or the topic poller applying a record whose expiry 
time has already passed, which it does when it lags behind by more than the 
time to live), and that entry has expired, the method returns {{null}}. It has 
not stored the caller's value and has not sent it to the topic. The interface 
Javadoc, the method's Javadoc and the state-store docs all say {{null}} means 
stored, so the caller acts as the owner of the key while no other instance (and 
no restart) can see its value, and the next {{putIfAbsent}} on that key also 
succeeds.

Expected: {{keys()}} and {{size()}} skip and evict expired entries; 
{{putIfAbsent}} returns {{null}} only when it stored and sent the value.

Suggested fix: in {{evictExpired()}} remove through the cache 
({{cache.remove(key, entry)}}, so a key put again meanwhile is kept); in 
{{putIfAbsent}} replace an expired entry atomically ({{cache.replace(key, 
expired, newEntry)}}) and, when that fails because the key changed meanwhile, 
look again.

Found by a code review of the Kafka repositories; both reproduce in unit tests 
with a {{MockProducer}} and no broker.

_Filed with Claude Code on behalf of allthingssecurity._



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to