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)

Reply via email to