shashank created CAMEL-25155:
--------------------------------
Summary: camel-spring-redis -
SpringRedisIdempotentRepository.add() checks the key and adds it in two round
trips and ignores the SADD result, so the same message id is processed by two
consumers at the same time
Key: CAMEL-25155
URL: https://issues.apache.org/jira/browse/CAMEL-25155
Project: Camel
Issue Type: Bug
Components: camel-spring-redis
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: the store decides which of two concurrent callers wins.
{{SpringRedisIdempotentRepository.add}} (line numbers of main):
{code:java}
public boolean add(String key) {
if (!contains(key)) { // :85
SISMEMBER
return setOperations.add(repositoryName, key) != null; // :86 SADD
} else {
return false;
}
}
{code}
Two consumers (threads of one node, or two nodes sharing the Redis set, which
is the reason to use this repository) that receive the same message id at the
same time both read "not a member" and both call {{SADD}}. {{SADD}} is atomic
and returns the number of members it added, 1 for the first caller and 0 for
the second, but the result is compared with {{null}}, which is true for both.
Both calls return true and the message is processed twice. The {{!= null}}
check makes the atomic command useless even though it already gives the right
answer.
The string based {{SpringRedisStringIdempotentRepository}} is not affected (it
uses {{SET NX}} through {{setIfAbsent}}).
h3. Reproduction
A local redis-server, {{RedisTemplate}} with Jedis and string serializers:
* route {{from("direct:in").idempotentConsumer(header("id"),
repo).process(count)}}, two threads send a message with the same id; a subclass
of the repository holds both calls after {{contains}} returned (a barrier, no
sleeps): the message is processed 2 times. 3 of 3 runs, and 20 of 20 in a loop.
Sequential control: processed once.
* no hooks: two repository instances with their own connection pools (two
nodes) and two threads each call {{add}} for the same 5000 keys: {{add}}
returned true more than once for 4984 to 5000 of the keys in 3 of 3 runs.
A TLA+ model of two consumers calling {{add}} (membership check and insert as
separate steps, as here) 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 (three consumers).
h3. Proposed fix
{code:java}
public boolean add(String key) {
// SADD is atomic and returns the number of members that were added (0 when
the key is already in the set)
Long added = setOperations.add(repositoryName, key);
return added != null && added > 0;
}
{code}
One round trip instead of two. The {{IdempotentRepository.add}} contract is
kept: true only when the key was not in the set; inside a pipeline or
transaction Spring Data Redis returns null and {{add}} returns false, as
before. 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. A unit test with a mocked {{SetOperations}} (member check false,
{{SADD}} returns 0) fails without the fix; the camel-spring-redis tests pass
(125). Against a local redis-server, two repository instances and four threads
adding the same 500 keys at a barrier added the first key twice without the fix
and every key once with it. {{remove}} has the same {{!= null}} comparison
(always true); it could return {{removed != null && removed > 0}} for the same
reason (not a behaviour problem for the EIP).
Affected: all versions (the same code in {{RedisIdempotentRepository}} at
camel-3.20.0 and 4.0.0, and in {{SpringRedisIdempotentRepository}} at 4.10.0,
4.14.0, 4.18.0, 4.22.0 and main).
Duplicate check (2026-09-30): JIRA text "SpringRedisIdempotentRepository"
(CAMEL-20768 flush on start, CAMEL-20767 Spring Boot creation, CAMEL-24421
deserialization filter), "RedisIdempotentRepository" (CAMEL-7541, CAMEL-9023,
2014-2015), component camel-spring-redis since 2023 (4 issues), text
"idempotent" with "atomic" (none for Redis); GitHub pull requests
"SpringRedisIdempotentRepository" (#25587-#25590 deserialization filter, #14165
flush on start), "redis idempotent": none.
_Filed with Claude Code on behalf of allthingssecurity._
--
This message was sent by Atlassian Jira
(v8.20.10#820010)