Federico Mariani created CAMEL-24133:
----------------------------------------

             Summary: camel-resilience4j / camel-microprofile-fault-tolerance - 
Circuit Breaker EIP corrupts in-flight exchange in pooled mode (releases 
original exchange instead of copy)
                 Key: CAMEL-24133
                 URL: https://issues.apache.org/jira/browse/CAMEL-24133
             Project: Camel
          Issue Type: Bug
          Components: eip, camel-microprofile-fault-tolerance
    Affects Versions: 4.21.0
            Reporter: Federico Mariani
         Attachments: 
FaultTolerancePooledProcessorExchangeFactoryIssueTest.java, 
ResiliencePooledProcessorExchangeFactoryIssueTest.java

The Circuit Breaker EIP corrupts in-flight exchanges when exchange pooling is 
enabled ({{camel.main.exchange-factory=pooled}}). Both implementations release 
the *original* in-flight exchange back to the processor exchange pool instead 
of the correlated copy:

* {{ResilienceProcessor.processTask()}} (camel-resilience4j)
* {{FaultToleranceProcessor.CircuitBreakerTask.call()}} 
(camel-microprofile-fault-tolerance)

{code:java}
// and release exchange back in pool
processorExchangeFactory.release(exchange);   // 'exchange' is the ORIGINAL 
in-flight exchange; 'copy' is leaked
{code}

Every other core EIP releases the derived exchange ({{Splitter}} releases 
{{copy}}, {{Enricher}} releases {{resourceExchange}}, {{MulticastProcessor}} 
releases {{pair.getExchange()}}).

{{PooledProcessorExchangeFactory.release()}} calls {{PooledExchange.done()}}, 
which wipes properties, exception and exchangeId, resets the message, fires the 
consumer's {{onDone}} hook, and then offers the still-live exchange into the 
per-processor pool.

Consequences with pooled mode enabled:
* the message body and all exchange properties are wiped right after the 
protected block: downstream steps receive a {{null}} body (both attached 
reproducers fail with {{Expected: <Bye World> but was: <null>}});
* the in-flight exchange can be handed out as the "copy" for the next message 
while the route is still using it, i.e. cross-exchange corruption under 
concurrency;
* the correlated copy is never released, so the pool never actually recycles 
copies.

Why this was never caught:
* {{ProducerTemplate}} creates a plain {{DefaultExchange}}, so the 
{{(PooledExchange)}} cast inside {{release()}} throws {{ClassCastException}}, 
which {{release()}} silently swallows. The bug is only visible when the 
exchange comes from a pooled consumer.
* The existing {{ResiliencePooledRouteOkTest}} sends via ProducerTemplate *and* 
only installs {{PooledExchangeFactory}}, not {{PooledProcessorExchangeFactory}} 
(camel-main installs both when {{camel.main.exchange-factory=pooled}}), so it 
exercises neither half of the bug.

The attached reproducers use a {{timer:}} consumer plus both pooled factories, 
i.e. exactly what camel-main configures:
* {{ResiliencePooledProcessorExchangeFactoryIssueTest.java}} 
(camel-resilience4j) - FAILS
* {{FaultTolerancePooledProcessorExchangeFactoryIssueTest.java}} 
(camel-microprofile-fault-tolerance) - FAILS

Introduced by CAMEL-16451 (3.x, 2021); affects all of 4.x.

Suggested fix: release the {{copy}} (when non-null) instead of the original 
exchange, in both processors.

_Filed by Claude Code on behalf of Federico Mariani (fmariani)_



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to