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 4fa0b6f1ce3d CAMEL-26314: camel-core - DefaultDebugger: a 
single-stepped exchange that fails ends its single step (#28573)
4fa0b6f1ce3d is described below

commit 4fa0b6f1ce3deb72df982df91ba8136d1eb9bb24
Author: Vignesh Nayak Manel <[email protected]>
AuthorDate: Wed Oct 28 11:40:24 2026 +0530

    CAMEL-26314: camel-core - DefaultDebugger: a single-stepped exchange that 
fails ends its single step (#28573)
    
    * CAMEL-26314: camel-core - DefaultDebugger: a single-stepped exchange that 
fails ends its single step
    
    DefaultDebugger removed a single-stepped exchange from singleSteps only on
    ExchangeCompletedEvent, but an exchange that fails raises 
ExchangeFailedEvent
    instead (DefaultUnitOfWork.done raises one or the other). Each failed 
exchange
    left its entry behind, and after 100 of them startSingleStepExchange refused
    every exchange, so the backlog debugger could no longer step.
    
    Treat ExchangeFailedEvent like ExchangeCompletedEvent in the single-step
    breakpoint and in the DebugEventNotifier fail safe, as the backlog 
debugger's
    StepBreakpoint already does. singleSteps is written from the routing 
threads of
    concurrent exchanges, so it is now a ConcurrentHashMap.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    
    * CAMEL-26314: camel-api - Debugger Javadoc: the default debugger also 
stops single stepping a failed exchange
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../main/java/org/apache/camel/spi/Debugger.java   |  2 +-
 .../camel/impl/debugger/DefaultDebugger.java       | 13 ++--
 .../DebugSingleStepFailedExchangeTest.java         | 83 ++++++++++++++++++++++
 3 files changed, 92 insertions(+), 6 deletions(-)

diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/Debugger.java 
b/core/camel-api/src/main/java/org/apache/camel/spi/Debugger.java
index e1f7c345bd75..a29465755a4e 100644
--- a/core/camel-api/src/main/java/org/apache/camel/spi/Debugger.java
+++ b/core/camel-api/src/main/java/org/apache/camel/spi/Debugger.java
@@ -118,7 +118,7 @@ public interface Debugger extends Service, 
CamelContextAware {
      * Stops the single step debug mode for the given exchange.
      * <p/>
      * <b>Notice:</b> The default implementation of the debugger is capable of 
auto stopping when the exchange is
-     * complete.
+     * complete or failed.
      *
      * @param exchangeId the exchange id
      */
diff --git 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultDebugger.java
 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultDebugger.java
index 98fbcf33b688..a722c06948dc 100644
--- 
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultDebugger.java
+++ 
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/debugger/DefaultDebugger.java
@@ -19,9 +19,9 @@ package org.apache.camel.impl.debugger;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import org.apache.camel.CamelContext;
@@ -36,6 +36,7 @@ import org.apache.camel.spi.CamelEvent;
 import org.apache.camel.spi.CamelEvent.ExchangeCompletedEvent;
 import org.apache.camel.spi.CamelEvent.ExchangeCreatedEvent;
 import org.apache.camel.spi.CamelEvent.ExchangeEvent;
+import org.apache.camel.spi.CamelEvent.ExchangeFailedEvent;
 import org.apache.camel.spi.Condition;
 import org.apache.camel.spi.Debugger;
 import org.apache.camel.spi.EventNotifier;
@@ -56,7 +57,8 @@ public class DefaultDebugger extends ServiceSupport 
implements Debugger {
     private final EventNotifier debugEventNotifier = new DebugEventNotifier();
     private final List<BreakpointConditions> breakpoints = new 
CopyOnWriteArrayList<>();
     private final int maxConcurrentSingleSteps = 100;
-    private final Map<String, Breakpoint> singleSteps = new 
HashMap<>(maxConcurrentSingleSteps);
+    // written from the routing threads of concurrent exchanges
+    private final Map<String, Breakpoint> singleSteps = new 
ConcurrentHashMap<>(maxConcurrentSingleSteps);
     private CamelContext camelContext;
 
     /**
@@ -153,7 +155,8 @@ public class DefaultDebugger extends ServiceSupport 
implements Debugger {
                 try {
                     breakpoint.onEvent(exchange, event, definition);
                 } finally {
-                    if (event instanceof ExchangeCompletedEvent) {
+                    // an exchange that fails raises ExchangeFailedEvent 
instead of ExchangeCompletedEvent
+                    if (event instanceof ExchangeCompletedEvent || event 
instanceof ExchangeFailedEvent) {
                         stopSingleStepExchange(exchange.getExchangeId());
                     }
                 }
@@ -408,8 +411,8 @@ public class DefaultDebugger extends ServiceSupport 
implements Debugger {
             Exchange exchange = aee.getExchange();
             onEvent(exchange, aee);
 
-            if (event instanceof ExchangeCompletedEvent) {
-                // fail safe to ensure we remove single steps when the 
Exchange is complete
+            if (event instanceof ExchangeCompletedEvent || event instanceof 
ExchangeFailedEvent) {
+                // fail safe to ensure we remove single steps when the 
Exchange is complete (or failed)
                 singleSteps.remove(exchange.getExchangeId());
             }
         }
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/interceptor/DebugSingleStepFailedExchangeTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/interceptor/DebugSingleStepFailedExchangeTest.java
new file mode 100644
index 000000000000..46605bf35962
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/interceptor/DebugSingleStepFailedExchangeTest.java
@@ -0,0 +1,83 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.processor.interceptor;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.Exchange;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.impl.debugger.DefaultDebugger;
+import org.apache.camel.support.BreakpointSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A single-stepped exchange that fails must end its single step, as one that 
completes does. The debugger single-steps
+ * at most 100 exchanges at the same time, so otherwise it refuses to 
single-step any exchange after 100 failed.
+ */
+public class DebugSingleStepFailedExchangeTest extends ContextTestSupport {
+
+    // the number of exchanges DefaultDebugger single-steps at the same time
+    private static final int MAX_CONCURRENT_SINGLE_STEPS = 100;
+
+    @Test
+    public void testFailedExchangeStartedInTheRouteEndsItsSingleStep() {
+        // as the backlog debugger does when it steps a suspended exchange
+        for (int i = 0; i <= MAX_CONCURRENT_SINGLE_STEPS; i++) {
+            Exchange failed = template.send("direct:start", e -> 
e.getIn().setBody("Kaboom"));
+            assertNotNull(failed.getException(), "the exchange should fail");
+            assertEquals(Boolean.TRUE, failed.getProperty("started"),
+                    "single stepping exchange " + (i + 1) + " should be 
accepted after " + i + " failed");
+        }
+    }
+
+    @Test
+    public void testFailedExchangeOfSingleStepBreakpointEndsItsSingleStep() {
+        context.getDebugger().addSingleStepBreakpoint(new BreakpointSupport() {
+        });
+
+        for (int i = 0; i < MAX_CONCURRENT_SINGLE_STEPS; i++) {
+            Exchange failed = template.send("direct:fail", e -> 
e.getIn().setBody("Kaboom"));
+            assertNotNull(failed.getException(), "the exchange should fail");
+        }
+
+        assertTrue(context.getDebugger().startSingleStepExchange("next", new 
BreakpointSupport() {
+        }), "single stepping should be accepted after " + 
MAX_CONCURRENT_SINGLE_STEPS + " single-stepped exchanges failed");
+    }
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                context.setDebugging(true);
+                context.setDebugger(new DefaultDebugger());
+
+                from("direct:start")
+                        .process(e -> e.setProperty("started", 
e.getContext().getDebugger()
+                                .startSingleStepExchange(e.getExchangeId(), 
new BreakpointSupport() {
+                                })))
+                        .throwException(new 
IllegalArgumentException("Forced"));
+
+                from("direct:fail")
+                        .throwException(new 
IllegalArgumentException("Forced"));
+            }
+        };
+    }
+}

Reply via email to