[
https://issues.apache.org/jira/browse/CAMEL-25215?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Claus Ibsen resolved CAMEL-25215.
---------------------------------
Resolution: Fixed
> camel-ehcache, camel-jcache - EhcacheKeyValueRepository and
> JCacheKeyValueRepository use the non-atomic default putIfAbsent, so
> KeyValueIdempotentRepository can add the same message id twice
> ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-25215
> URL: https://issues.apache.org/jira/browse/CAMEL-25215
> Project: Camel
> Issue Type: Bug
> Components: camel-ehcache, camel-jcache
> Reporter: shashank
> Assignee: shashank
> Priority: Minor
> Fix For: 4.23.0
>
>
> {{KeyValueRepository}} (new in 4.23, CAMEL-24463) has default implementations
> of {{putIfAbsent}}, {{replace}} and {{delete(key, expectedValue)}} that are
> documented as not atomic ({{get}} followed by {{put}} or {{delete}}), and
> implementations backed by a store with atomic operations should override
> them. The Caffeine, Hazelcast, Infinispan, Cassandra, JDBC, JPA, Kafka and
> Redis repositories override {{putIfAbsent}} (the operation the idempotent
> adapter uses) with an atomic operation of their store; Caffeine, Hazelcast,
> Infinispan, Cassandra and JDBC override all three.
> {{EhcacheKeyValueRepository}} and {{JCacheKeyValueRepository}} do not,
> although Ehcache and JCache have an atomic {{putIfAbsent}}, {{replace(k, old,
> new)}} and {{remove(k, v)}}.
> {{KeyValueIdempotentRepository.add}} is {{repository.putIfAbsent(key, TRUE)
> == null}} (:87), and the Idempotent Consumer EIP (eager, the default) calls
> {{add}} without a lock. With one of these two repositories, two exchanges
> with the same message id that reach the EIP at the same time (concurrent
> consumers, a parallel split) can both read "absent" and both be added, so the
> duplicate is processed. This is the defect of CAMEL-25156 (Caffeine) and
> CAMEL-25155 (Spring Redis) in the key-value adapter. The {{putIfAbsent}}
> operation of the state-store component has the same race. With
> {{JCacheKeyValueRepository}} on a clustered JCache provider (Hazelcast,
> Infinispan) the race is between nodes as well.
> A related race: {{get}}, {{contains}} and {{keys}} remove an expired entry
> lazily with an unconditional {{cache.remove(key)}}, so a concurrent writer's
> new entry for the key can be removed.
> h3. Reproduction
> For each repository, two threads call
> {{KeyValueIdempotentRepository.add("message-1")}} on a repository whose cache
> is wrapped (a dynamic proxy) so that {{get}} returns only when both threads
> have read the key: both calls return true ({{[true, true]}}). 3 of 3 runs for
> both repositories. Expected: one true, one false.
> h3. Proposed fix
> Override the three operations in both repositories with the atomic operations
> of the cache:
> * {{putIfAbsent}}: the cache's {{putIfAbsent}}; an existing entry that has
> expired (these repositories expire entries lazily) counts as absent and is
> replaced with {{replace(key, expired, new)}}, retrying if it changed in the
> meantime.
> * {{replace}} and {{delete(key, expectedValue)}}: read the entry, compare its
> value, then {{replace(key, current, new)}} / {{remove(key, current)}},
> retrying if it changed in the meantime.
> * the lazy removal of an expired entry uses {{remove(key, expiredEntry)}}.
> * {{KeyValueTtlValue}} gets {{equals}} and {{hashCode}} based on a random
> token set per write and the expiry time, not on the wrapped value. A cache
> that stores copies of its values (the JCache default {{storeByValue=true}},
> Ehcache with a value copier or an off-heap tier) compares the stored copy
> with the expected entry, and a wrapped value may have no value equality (a
> {{byte[]}}, a POJO without {{equals}}). The token survives the copy, so a
> compare-and-swap fails only when another thread changed the entry, and the
> retry loops end. An equality on the wrapped value would make {{putIfAbsent}}
> spin forever on an expired {{byte[]}} entry in such a cache.
> With the fix both tests return one true and one false (3 of 3). New tests:
> the concurrency test per repository; {{putIfAbsent}} with a new, an existing
> and an expired key (the existing tests had none for {{putIfAbsent}}); and a
> by-value test per repository (Ehcache with a serializing value copier; a
> JCache whose {{replace}} and {{remove(k, v)}} compare the stored copy with
> {{equals}}, as the Hazelcast provider of the module tests compares serialized
> bytes) with an expired {{byte[]}} entry. With an equality on the wrapped
> value, {{putIfAbsent}} does not return within 5 seconds and {{get}} leaves
> the expired entry in the cache (3 of 3 for both repositories). With the fix
> camel-ehcache passes 69 tests, camel-jcache 91, and the key-value tests of
> camel-support (69) and camel-core (20) pass.
> Affected: main only (4.23.0-SNAPSHOT); the key-value repositories are not
> released yet.
> Duplicate check (2026-09-30): JIRA text "EhcacheKeyValueRepository",
> "JCacheKeyValueRepository", "KeyValueIdempotentRepository" (CAMEL-24463,
> CAMEL-23239, CAMEL-24953, CAMEL-24954, none about atomicity); GitHub pull
> requests "EhcacheKeyValueRepository", "JCacheKeyValueRepository",
> "KeyValueTtlValue", "KeyValueRepository putIfAbsent": only #25816 and #26086
> that added them. No open pull request touches these files.
> _Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)