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)