[ 
https://issues.apache.org/jira/browse/CAMEL-26395?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18137470#comment-18137470
 ] 

Claus Ibsen commented on CAMEL-26395:
-------------------------------------

[~mgabr] thanks for the report, the problem is real. Apart from a crash between 
the two calls, a failing {{persistedCache.put}} (network error, Redis error) 
loses the group the same way: the remove already went through, and the 
exception goes back to the incoming exchange.

Wrapping the optimistic branch in an {{RTransaction}} would break the 
optimistic locking though. In Redisson 4.8.0 the transactional {{remove(key, 
value)}} compares the value on the client, and at commit sends the remove in 
MULTI/EXEC without checking its result. The optimistic {{add()}} uses plain 
{{replace}}/{{putIfAbsent}}, which do not take the transaction lock. So when 
another node adds a message to the group in between, the remove silently does 
nothing but the {{put}} into the recovery map still runs: the old aggregate 
goes downstream and the group completes again later, instead of an 
{{OptimisticLockingException}} and a retry. Today {{cache.remove(key, holder)}} 
is a compare-and-delete on the server (a Lua script inside Redisson), and that 
must stay.

Suggested approach, stage in the recovery map first:
# {{persistedCache.put(exchangeId, holder)}} when {{useRecovery}}
# {{cache.remove(key, holder)}} as today
# if the remove fails, remove the staged entry again 
({{persistedCache.remove(exchangeId)}}) and throw {{OptimisticLockingException}}

The worst case becomes a duplicate instead of a loss, which is what the 
recovery feature gives anyway, and it keeps working on Redis Cluster. (One Lua 
script doing compare + HSET + HDEL would be strictly atomic, but the two maps 
hash to different slots on a cluster, so it would need renamed keys with a hash 
tag; not worth it here.)

For the tests in {{RedisAggregationRepositoryOperationsIT}}:
* make the {{put}} fail without a crash, e.g. store a string under the 
persistence map name ({{redisson.getBucket(persistenceMapName).set("x")}}) so 
it fails with WRONGTYPE, call the optimistic {{remove()}}, and assert it throws 
and the group is still in the active map (fails on main today)
* a remove with a stale holder throws {{OptimisticLockingException}} and leaves 
nothing in the recovery map

A PR is very welcome, as you offered. Did you see this in production or find it 
reading the code, and do you run a single Redis or Redis Cluster?

_Claude Code on behalf of Claus Ibsen (davsclaus)_


> camel-redis - Potential exchange loss in RedisAggregationRepository 
> optimistic removal
> --------------------------------------------------------------------------------------
>
>                 Key: CAMEL-26395
>                 URL: https://issues.apache.org/jira/browse/CAMEL-26395
>             Project: Camel
>          Issue Type: Bug
>            Reporter: Markus Gabriel
>            Priority: Major
>
> In {{{}RedisAggregationRepository.remove(){}}}, the optimistic branch 
> executes the delete and recovery staging as two separate, non-atomic 
> operations:
> {code:java}
> if (!cache.remove(key, holder)) {
>     throw new OptimisticLockingException();
> }
> if (useRecovery) {
>     persistedCache.put(exchange.getExchangeId(), holder);
> }{code}
> If the JVM crashes or gets killed right between {{cache.remove(...)}} and 
> {{persistedCache.put(...)}} (or if the {{put}} throws an exception), the 
> exchange is permanently removed from the active cache without ever making it 
> into the recovery store.
> The non-optimistic branch right below it already wraps this handoff in an 
> {{RTransaction}} to keep it atomic. It seems like the optimistic branch 
> should do the same to prevent dropping exchanges.
> I'm happy to open a PR for this if the approach makes sense.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to