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)