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"));
+ }
+ };
+ }
+}