Federico Mariani created CAMEL-24136:
----------------------------------------

             Summary: Circuit Breaker EIP - umbrella for medium findings (FT 
exchange-property contract, parity gaps, dead option, docs)
                 Key: CAMEL-24136
                 URL: https://issues.apache.org/jira/browse/CAMEL-24136
             Project: Camel
          Issue Type: Bug
          Components: eip, camel-microprofile-fault-tolerance
    Affects Versions: 4.21.0
            Reporter: Federico Mariani
         Attachments: camel-circuitbreaker-eip-review-2026-07.md

Umbrella for the medium-severity findings from a review of the Circuit Breaker 
EIP (core model, camel-resilience4j, camel-microprofile-fault-tolerance). Full 
review document attached. Related bugs filed separately: CAMEL-24133 
(pooled-mode exchange corruption), CAMEL-24134 (timeout write-back race), 
CAMEL-24135 (JMX metric).

h3. M1. FT: no-fallback timeout/short-circuit paths never set the documented 
exchange properties

When there is no fallback, a guard exception lands in 
{{FaultToleranceProcessor.process()}} {{catch (Exception e) -> 
exchange.setException(e)}}; {{CamelCircuitBreakerResponseTimedOut}} and 
{{...SuccessfulExecution=false}} are never set. The branch that would set them 
(inside {{CircuitBreakerFallbackTask}}, the {{fallbackProcessor == null}} 
block) is unreachable dead code, because the fallback task is only acquired 
when a fallback exists. The resilience4j implementation sets these properties 
in the same scenario. No test in either module asserts {{RESPONSE_TIMED_OUT}} 
at all.

h3. M2. FT: guard-level exceptions invisible to the fallback 
({{CamelExceptionCaught}} = null)

Guard-raised exceptions (SmallRye {{TimeoutException}}, 
{{CircuitBreakerOpenException}}, bulkhead rejection) are never stored on the 
exchange before the fallback runs, so the fallback route sees 
{{exchange.getException() == null}} and {{EXCEPTION_CAUGHT}} is set to null. 
Only exceptions thrown by the protected processor itself are visible. In the 
resilience4j implementation the throwable is always available to the recover 
function. Fallback routes that branch on the cause behave differently between 
the two implementations.

h3. M3. FT feature-parity gaps vs the resilience4j implementation

* no {{recordExceptions}} / {{ignoreExceptions}} equivalent - SmallRye's 
circuit breaker builder supports {{failOn()}} / {{skipOn()}}, so this is 
wireable; {{CamelCircuitBreakerResponseIgnored}} is never set by FT;
* no {{throwExceptionWhenHalfOpenOrOpenState}} equivalent (open circuit + no 
fallback always silently continues routing);
* live metrics: FT JMX exposes only configuration echoes; {{getFailureRate}} is 
described as "the current failure rate in percentage" but returns the 
*configured* ratio as a 0-1 fraction. The FT dev console is likewise 
config-only, while the resilience4j console shows live state and counters.

h3. M4. FT: {{timeoutPoolSize}} option is dead

Parsed by the reifier and echoed in JMX, but never applied to the 
{{TypedGuard}} (the standalone TypedGuard API has no pool-size knob; the old 
timer-thread wiring was dropped in the CAMEL-21857 TypedGuard migration). 
Either wire it or deprecate it with an upgrade-guide note.

h3. M5. resilience4j: circuit breaker state and metrics are destroyed on route 
stop/suspend

{{ResilienceProcessor.doStop()}} removes the breaker/timelimiter/bulkhead from 
the registries, so a route suspend/resume or stop/start resets breaker state 
(an OPEN breaker becomes CLOSED) and zeroes metrics. FT does the same via 
{{CircuitBreakerMaintenance.reset(id)}}. If intentional, it should at least be 
documented.

h3. M6. Documentation errors

* {{resilience4j-eip.adoc}} "supports two options" table lists 
{{resilienceConfiguration}} / {{resilienceConfigurationRef}} - neither exists; 
the actual names are {{resilience4jConfiguration}} and {{configuration}};
* {{circuitBreaker-eip.adoc}} says "By default, the timeout request is just 
1000ms" - timeout is *disabled* by default; 1000ms is only the default duration 
once enabled;
* {{resilience4j-eip.adoc}} "If there was no fallback, then the circuit breaker 
will throw an exception" is wrong for the open-circuit case: by default 
({{throwExceptionWhenHalfOpenOrOpenState=false}}) the message continues routing 
unchanged without an exception (behavior confirmed by {{*RouteRejectedTest}} in 
both implementations).

h3. M7. Smaller observations

* FT {{getCircuitBreakerState()}} uses exception-as-control-flow per message; 
with a user-supplied {{TypedGuard}} the {{CircuitBreakerMaintenance}} lookup 
throws on every exchange;
* when an existing breaker/guard is referenced ({{circuitBreaker}} / 
{{typedGuard}} option), the remaining inline configuration is silently ignored 
(FT: timeout/bulkhead; resilience4j: CB config except record/ignore predicates) 
- should be documented or logged;
* ignored/not-recorded exceptions are cleared and the message continues with 
the request body; tested and intended, but it diverges from resilience4j's own 
semantics (where ignored exceptions still propagate) and deserves an explicit 
doc note.

_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