This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new b7cfc0f148a2 CAMEL-24148: Fix saga option values overwritten on 
repeated step enlistment
b7cfc0f148a2 is described below

commit b7cfc0f148a2b0cdd9d480512d0a9d051a2be305
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Jul 17 14:09:26 2026 +0200

    CAMEL-24148: Fix saga option values overwritten on repeated step enlistment
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../apache/camel/processor/SagaOptionsTest.java    | 40 ++++++++++++++++
 .../apache/camel/saga/InMemorySagaCoordinator.java | 53 ++++++++++++----------
 2 files changed, 68 insertions(+), 25 deletions(-)

diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/SagaOptionsTest.java 
b/core/camel-core/src/test/java/org/apache/camel/processor/SagaOptionsTest.java
index aa7900bec82a..676328b27305 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/processor/SagaOptionsTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/SagaOptionsTest.java
@@ -16,13 +16,18 @@
  */
 package org.apache.camel.processor;
 
+import java.util.List;
+import java.util.stream.Collectors;
+
 import org.apache.camel.ContextTestSupport;
 import org.apache.camel.Exchange;
 import org.apache.camel.builder.RouteBuilder;
 import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.model.SagaPropagation;
 import org.apache.camel.saga.InMemorySagaService;
 import org.junit.jupiter.api.Test;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.fail;
 
 public class SagaOptionsTest extends ContextTestSupport {
@@ -60,6 +65,27 @@ public class SagaOptionsTest extends ContextTestSupport {
         compensate.assertIsSatisfied();
     }
 
+    @Test
+    public void testOptionsPreservedOnRepeatedEnlistment() throws Exception {
+        MockEndpoint compensate = getMockEndpoint("mock:compensate");
+        compensate.expectedMessageCount(2);
+
+        try {
+            template.sendBody("direct:workflow-repeated", "go");
+            fail("Should throw an exception");
+        } catch (Exception ex) {
+            // OK
+        }
+
+        compensate.assertIsSatisfied();
+
+        List<String> itemIds = compensate.getReceivedExchanges().stream()
+                .map(e -> e.getMessage().getHeader("itemId", String.class))
+                .sorted()
+                .collect(Collectors.toList());
+        assertEquals(List.of("item-A", "item-B"), itemIds);
+    }
+
     @Override
     protected RouteBuilder createRouteBuilder() {
 
@@ -69,6 +95,20 @@ public class SagaOptionsTest extends ContextTestSupport {
 
                 context.addService(new InMemorySagaService());
 
+                from("direct:workflow-repeated").saga()
+                        .setHeader("itemId", constant("item-A"))
+                        .to("direct:enlistItem")
+                        .setHeader("itemId", constant("item-B"))
+                        .to("direct:enlistItem")
+                        .process(ex -> {
+                            throw new RuntimeException("forced failure");
+                        });
+
+                
from("direct:enlistItem").saga().propagation(SagaPropagation.MANDATORY)
+                        .option("itemId", header("itemId"))
+                        .compensation("mock:compensate")
+                        .log("Enlisted ${header.itemId}");
+
                 from("direct:workflow").saga().option("id", 
constant("myheader")).option("name", header("myname"))
                         
.completion("mock:complete").compensation("mock:compensate")
                         
.choice().when(body().isEqualTo("compensate")).process(ex -> {
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/saga/InMemorySagaCoordinator.java
 
b/core/camel-support/src/main/java/org/apache/camel/saga/InMemorySagaCoordinator.java
index f6996eda5cfc..85f900ca6dc2 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/saga/InMemorySagaCoordinator.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/saga/InMemorySagaCoordinator.java
@@ -23,7 +23,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicReference;
@@ -57,16 +56,14 @@ public class InMemorySagaCoordinator implements 
CamelSagaCoordinator {
     private final CamelContext camelContext;
     private final InMemorySagaService sagaService;
     private final String sagaId;
-    private final List<CamelSagaStep> steps;
-    private final Map<CamelSagaStep, Map<String, Object>> optionValues;
+    private final List<StepEnlistment> enlistments;
     private final AtomicReference<Status> currentStatus;
 
     public InMemorySagaCoordinator(CamelContext camelContext, 
InMemorySagaService sagaService, String sagaId) {
         this.camelContext = ObjectHelper.notNull(camelContext, "camelContext");
         this.sagaService = ObjectHelper.notNull(sagaService, "sagaService");
         this.sagaId = ObjectHelper.notNull(sagaId, "sagaId");
-        this.steps = new CopyOnWriteArrayList<>();
-        this.optionValues = new ConcurrentHashMap<>();
+        this.enlistments = new CopyOnWriteArrayList<>();
         this.currentStatus = new AtomicReference<>(Status.RUNNING);
     }
 
@@ -84,27 +81,26 @@ public class InMemorySagaCoordinator implements 
CamelSagaCoordinator {
             return res;
         }
 
-        this.steps.add(step);
-
+        Map<String, Object> values = new HashMap<>();
         if (!step.getOptions().isEmpty()) {
-            optionValues.putIfAbsent(step, new ConcurrentHashMap<>());
-            Map<String, Object> values = optionValues.computeIfAbsent(step, k 
-> new HashMap<>());
-            for (String option : step.getOptions().keySet()) {
-                Expression expression = step.getOptions().get(option);
+            for (Map.Entry<String, Expression> entry : 
step.getOptions().entrySet()) {
+                Expression expression = entry.getValue();
                 if (expression != null) {
                     try {
                         Object value = expression.evaluate(exchange, 
Object.class);
                         if (value != null) {
-                            values.put(option, value);
+                            values.put(entry.getKey(), value);
                         }
                     } catch (Exception ex) {
                         return CompletableFuture.supplyAsync(() -> {
-                            throw new RuntimeCamelException("Cannot evaluate 
saga option '" + option + "'", ex);
+                            throw new RuntimeCamelException(
+                                    "Cannot evaluate saga option '" + 
entry.getKey() + "'", ex);
                         });
                     }
                 }
             }
         }
+        this.enlistments.add(new StepEnlistment(step, values));
 
         if (step.getTimeoutInMilliseconds().isPresent()) {
             sagaService.getExecutorService().schedule(() -> {
@@ -188,11 +184,11 @@ public class InMemorySagaCoordinator implements 
CamelSagaCoordinator {
             final Exchange exchange,
             Function<CamelSagaStep, Optional<Endpoint>> endpointExtractor, 
String description) {
         CompletableFuture<Boolean> result = 
CompletableFuture.completedFuture(true);
-        for (CamelSagaStep step : reversed(steps)) {
-            Optional<Endpoint> endpoint = endpointExtractor.apply(step);
+        for (StepEnlistment enlistment : reversed(enlistments)) {
+            Optional<Endpoint> endpoint = 
endpointExtractor.apply(enlistment.step);
             if (endpoint.isPresent()) {
                 result = result.thenCompose(
-                        prevResult -> doFinalize(exchange, endpoint.get(), 
step, 0, description)
+                        prevResult -> doFinalize(exchange, endpoint.get(), 
enlistment, 0, description)
                                 .thenApply(res -> prevResult && res));
             }
         }
@@ -206,8 +202,8 @@ public class InMemorySagaCoordinator implements 
CamelSagaCoordinator {
     }
 
     private CompletableFuture<Boolean> doFinalize(
-            Exchange exchange, Endpoint endpoint, CamelSagaStep step, int 
doneAttempts, String description) {
-        Exchange target = createExchange(exchange, endpoint, step);
+            Exchange exchange, Endpoint endpoint, StepEnlistment enlistment, 
int doneAttempts, String description) {
+        Exchange target = createExchange(exchange, endpoint, enlistment);
 
         return CompletableFuture.supplyAsync(() -> {
             Exchange res = sagaService.getProducerTemplate().send(endpoint, 
target);
@@ -229,7 +225,7 @@ public class InMemorySagaCoordinator implements 
CamelSagaCoordinator {
             } else {
                 CompletableFuture<Boolean> future = new CompletableFuture<>();
                 sagaService.getExecutorService().schedule(() -> {
-                    doFinalize(target, endpoint, step, currentAttempt, 
description).whenComplete((res, ex) -> {
+                    doFinalize(target, endpoint, enlistment, currentAttempt, 
description).whenComplete((res, ex) -> {
                         if (ex != null) {
                             future.completeExceptionally(ex);
                         } else {
@@ -243,7 +239,7 @@ public class InMemorySagaCoordinator implements 
CamelSagaCoordinator {
     }
 
     @SuppressWarnings("deprecation")
-    private Exchange createExchange(Exchange parent, Endpoint endpoint, 
CamelSagaStep step) {
+    private Exchange createExchange(Exchange parent, Endpoint endpoint, 
StepEnlistment enlistment) {
         Exchange answer = endpoint.createExchange();
         answer.getMessage().setHeader(Exchange.SAGA_LONG_RUNNING_ACTION, 
getId());
         answer.getExchangeExtension().setSagaLongRunningAction(getId());
@@ -254,11 +250,8 @@ public class InMemorySagaCoordinator implements 
CamelSagaCoordinator {
             answer.setProperty(ExchangePropertyKey.OTEL_ACTIVE_SPAN, span);
         }
 
-        Map<String, Object> values = optionValues.get(step);
-        if (values != null) {
-            for (Map.Entry<String, Object> entry : values.entrySet()) {
-                answer.getMessage().setHeader(entry.getKey(), 
entry.getValue());
-            }
+        for (Map.Entry<String, Object> entry : 
enlistment.optionValues.entrySet()) {
+            answer.getMessage().setHeader(entry.getKey(), entry.getValue());
         }
         return answer;
     }
@@ -268,4 +261,14 @@ public class InMemorySagaCoordinator implements 
CamelSagaCoordinator {
         Collections.reverse(reversed);
         return reversed;
     }
+
+    private static class StepEnlistment {
+        final CamelSagaStep step;
+        final Map<String, Object> optionValues;
+
+        StepEnlistment(CamelSagaStep step, Map<String, Object> optionValues) {
+            this.step = step;
+            this.optionValues = optionValues;
+        }
+    }
 }

Reply via email to