shashank created CAMEL-25156:
--------------------------------

             Summary: camel-caffeine - CaffeineIdempotentRepository.add() is a 
check-then-put, so two concurrent exchanges with the same message id both pass 
the Idempotent Consumer
                 Key: CAMEL-25156
                 URL: https://issues.apache.org/jira/browse/CAMEL-25156
             Project: Camel
          Issue Type: Bug
          Components: camel-caffeine
            Reporter: shashank


The Idempotent Consumer EIP (eager, the default) calls {{repository.add(key)}} 
without a lock and processes the message only when it returns true, so {{add}} 
must be atomic. {{MemoryIdempotentRepository}} holds a lock around its check 
and put; the other cache based repositories use an atomic {{putIfAbsent}}. 
{{CaffeineIdempotentRepository.add}} (line numbers of main) does not:
{code:java}
public boolean add(String key) {
    if (cache.asMap().containsKey(key)) {    // :61
        return false;
    } else {
        cache.put(key, true);                // :64
        return true;
    }
}
{code}
Two exchanges with the same id that reach the EIP at the same time (a consumer 
with concurrent consumers, such as seda, jms, kafka {{consumersCount}}, or a 
parallel split) can both read "absent" and both return true, so the duplicate 
is processed.

h3. Reproduction

* route {{from("direct:in").idempotentConsumer(header("id"), 
repo).process(count)}}, two threads send a message with the same id; the 
repository's cache is replaced (reflection, before start) by a delegating 
Caffeine cache whose {{asMap().containsKey}} waits on a barrier after it read 
the answer, so both calls are between the check and the put: the message is 
processed 2 times. 3 of 3 runs, and 20 of 20 in a loop. Sequential control: 
processed once.
* no hooks: 8 threads call {{add}} for the same 20000 keys: {{add}} returned 
true more than once for 3536, 1793 and 1338 keys in 3 runs.

A TLA+ model of two consumers calling {{add}} with the check and the put as 
separate steps violates "the message is not processed by two consumers at the 
same time"; with an atomic add it holds, also with a failed processing that 
removes the key and a redelivery.

h3. Proposed fix

{code:java}
public boolean add(String key) {
    // atomic, so two exchanges with the same key cannot both be added
    return cache.asMap().putIfAbsent(key, Boolean.TRUE) == null;
}
{code}
With the fix the route test processes the message once (3 of 3) and the stress 
test finds no key added twice (3 of 3); the sequential control is unchanged and 
the existing tests of the repository pass 
({{CaffeineIdempotentRepositoryTest}}, 
{{CaffeineIdempotentRepositoryWithSplitTest}}). A deterministic unit test (two 
{{add}} calls held on the key's entry by a {{compute}} of the cache map after 
they checked for the key) fails without the fix (both return true) and passes 
with it; camel-caffeine passes 81 tests.

Affected: all versions (the same code at camel-3.20.0, 4.0.0, 4.10.0, 4.14.0, 
4.18.0, 4.22.0 and main).

Duplicate check (2026-09-30): JIRA text "CaffeineIdempotentRepository" (none), 
component camel-caffeine since 2025 (CAMEL-23411 object filter, CAMEL-22759 
test context), text "idempotent" with "atomic" (none for Caffeine); GitHub pull 
requests "CaffeineIdempotentRepository", "caffeine idempotent": none.

_Filed with Claude Code on behalf of allthingssecurity._




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to