[ 
https://issues.apache.org/jira/browse/CAMEL-25155?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Claus Ibsen resolved CAMEL-25155.
---------------------------------
    Fix Version/s: 4.23.0
       Resolution: Fixed

The fix is merged on main, so it is in Camel 4.23.0:
* 371ea2edf7c8 CAMEL-25155: camel-spring-redis - make 
SpringRedisIdempotentRepository.add atomic
* 54d75cc6da1b CAMEL-25155: camel-spring-redis - remove an unused stub from the 
idempotent repository test

Resolving, as the ticket was not updated when the PR was merged.

_Claude Code on behalf of Claus Ibsen_

> 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
>            Priority: Minor
>             Fix For: 4.23.0
>
>
> 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