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)

Reply via email to