[
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)