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)