Federico Mariani created CAMEL-24151:
----------------------------------------
Summary: camel-saga/camel-lra: medium and low findings from Saga
EIP code review (July 2026)
Key: CAMEL-24151
URL: https://issues.apache.org/jira/browse/CAMEL-24151
Project: Camel
Issue Type: Bug
Components: camel-lra, camel-core
Reporter: Federico Mariani
Umbrella for the medium/low findings of a July 2026 code review of the Saga EIP
(model, reifier, processors, in-memory service, saga component, camel-lra). The
high-severity findings are filed as separate issues.
h3. Medium
* *LRAClient accepts only HTTP 200 and throws context-free errors*:
{{join}}/{{complete}}/{{compensate}} test {{statusCode() != HTTP_OK}}
(LRAClient.java:141,158,175). Other 2xx codes are legitimate per coordinator
implementations; 404/410 for an already-finished LRA (e.g. compensated by
timeout) is a common, meaningful case. Users get a bare
{{RuntimeCamelException("Cannot complete LRA")}} with no status code, response
body or LRA id. Fix: accept the 2xx range, include status/body/LRA URL in
messages.
* *In-memory step timeout timers never cancelled*: every {{beginStep}} with a
timeout schedules a compensation task (InMemorySagaCoordinator.java:109-116)
that is not cancelled on completion. Functionally harmless (CAS fails) but
retains the coordinator and the enlisting Exchange until the timer fires (hours
for long timeouts); under exchange pooling the captured exchange may have been
recycled when the timer runs. Fix: keep and cancel the {{ScheduledFuture}};
extract only what is needed from the exchange.
* *In-memory status machine loose ends*: terminal status is set in
{{thenApply}} (skipped if the chain completes exceptionally - coordinator hangs
in COMPENSATING/COMPLETING); {{doCompensate}} marks COMPENSATED even when steps
exhausted all retries (WARN only). The EIP doc says "a manual intervention
process should be triggered" - there is no hook, event or distinct status (e.g.
COMPENSATION_FAILED) for it.
* *MANUAL completion without a timeout silently leaks the saga*: docs say
MANUAL should always be combined with a timeout and mention a possible default
timeout; nothing enforces or warns, and no default-timeout mechanism exists in
either implementation. Fix: fail fast (or WARN) at reifier time; optionally add
a configurable default timeout.
* *ifNotException can swallow synchronous exceptions and hang the exchange*:
the propagation processors run continuation code inside {{whenComplete}}
lambdas (SagaProcessor.java:155-172). If {{code.run()}} throws synchronously
the exception disappears into the already-completed future and the
{{AsyncCallback}} is never invoked. {{LRASagaCoordinator}} defensively wraps
exactly one such case (LRASagaCoordinator.java:40-47); the general risk
remains. Fix: try/catch around {{code.run()}} -> {{exchange.setException(t);
callback.done(false)}}.
* *LRASagaService.setCamelContext adds participant REST routes as a side effect
of the setter* (LRASagaService.java:121-131): routes are added even if the
service is never started, and failures (e.g. no rest component) surface from an
unexpected place. Route setup belongs in {{doStart}}.
* *camel-lra: single shared URI allowlist, never pruned*: {{verifyRequest}}
validates against one combined set of all compensation and completion URIs
(LRASagaRoutes.java:135-139), so a completion callback may invoke a
compensation URI and vice versa, and URIs from removed routes stay allowed for
the context lifetime.
h3. Low / cleanups
* {{SagaProcessor.toString()}} returns the literal string "id"
(SagaProcessor.java:146-148).
* {{InMemorySagaCoordinator.beginStep}} does {{putIfAbsent(step, new
ConcurrentHashMap<>())}} immediately followed by {{computeIfAbsent(step, k ->
new HashMap<>())}} - the second mapping function is dead code.
* Failed futures built as {{CompletableFuture.supplyAsync(() -> { throw ex;
})}} (InMemorySagaCoordinator.java:101-103, LRASagaCoordinator.java:44-46) burn
a pool task (the first on the common ForkJoinPool) - use
{{CompletableFuture.failedFuture(ex)}}.
* {{LRAClient.join}} appends the time limit as {{lra + "?" + "TimeLimit=" +
ms}} (LRAClient.java:127-129) - double-{{?}} URL if the LRA id URI ever carries
a query string.
* Missing {{coordinatorUrl}} surfaces as {{IllegalStateException("Host not
set")}} from {{LRAUrlBuilder}} during doStart; missing {{localParticipantUrl}}
only fails at the first join. Validate both at doStart with messages naming the
configuration options.
* {{LRASagaService.registerStep}} uses {{Optional.map(set::add)}} for a side
effect with ignored return - use {{ifPresent}}.
* {{SagaProcessorBuilder}}/{{SagaProcessor}} accept a {{CamelContext}} that is
never stored or used.
* The EIP doc claims "the Saga EIP implementation may have a default timeout
set on all Sagas" - matches no shipped implementation.
* {{SagaProducer}}'s "not bound to a saga context: cannot complete" message is
hardcoded - {{saga:compensate}} failures also say "cannot complete"
(SagaProducer.java:49-50).
Found during a broader Saga EIP / camel-lra code review (July 2026).
_Claude Code on behalf of Croway_
--
This message was sent by Atlassian Jira
(v8.20.10#820010)