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 5dd7712722c8 CAMEL-26929: camel-console - the send console bounds the 
send by its timeout, and camel cmd send --timeout is taken as given (#29935)
5dd7712722c8 is described below

commit 5dd7712722c83341ae1150d82508910b942c2500
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 14 20:50:59 2026 +0100

    CAMEL-26929: camel-console - the send console bounds the send by its 
timeout, and camel cmd send --timeout is taken as given (#29935)
    
    The timeout of the send console was for a poll only, so a send to 
direct:nope waited the 30
    seconds of direct for a consumer and the CLI gave up first (exit 124). The 
send is now bounded
    by the timeout and answers with status timeout, a direct endpoint that no 
route consumes fails
    at once, and camel cmd send no longer raises a timeout below 5 seconds to 5 
seconds.
    
    CAMEL-26929: camel-console - roomier timing bounds in SendDevConsoleTest, 
and a negative --timeout is a usage error too
    
    CAMEL-26929: camel-console - an interrupted send keeps the interrupt flag
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../apache/camel/catalog/dev-consoles-openapi.json |  2 +-
 .../apache/camel/catalog/dev-consoles/send.json    |  2 +-
 .../org/apache/camel/dev-console/send.json         |  2 +-
 .../apache/camel/impl/console/SendDevConsole.java  | 87 +++++++++++++++++-----
 .../camel/impl/console/SendDevConsoleTest.java     | 82 ++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  4 +
 .../core/commands/action/CamelSendAction.java      |  7 +-
 .../commands/action/CamelSendActionResultTest.java | 21 +++++-
 8 files changed, 182 insertions(+), 25 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
index 71c40d466b4a..56f159f76514 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles-openapi.json
@@ -8223,7 +8223,7 @@
                                                                        
"pollTimeout": {
                                                                                
"type": "integer",
                                                                                
"default": 20000,
-                                                                               
"description": "Timeout when using poll mode"
+                                                                               
"description": "Timeout of the send, or of the poll in poll mode, in millis"
                                                                        }
                                                                }
                                                        }
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/send.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/send.json
index cb8dd24d3c59..39513b204c46 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/send.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/dev-consoles/send.json
@@ -118,7 +118,7 @@
       "autowired": false,
       "secret": false,
       "defaultValue": 20000,
-      "description": "Timeout when using poll mode"
+      "description": "Timeout of the send, or of the poll in poll mode, in 
millis"
     }
   },
   "response": {
diff --git 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/send.json
 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/send.json
index cb8dd24d3c59..39513b204c46 100644
--- 
a/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/send.json
+++ 
b/core/camel-console/src/generated/resources/META-INF/org/apache/camel/dev-console/send.json
@@ -118,7 +118,7 @@
       "autowired": false,
       "secret": false,
       "defaultValue": 20000,
-      "description": "Timeout when using poll mode"
+      "description": "Timeout of the send, or of the poll in poll mode, in 
millis"
     }
   },
   "response": {
diff --git 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/SendDevConsole.java
 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SendDevConsole.java
index 0ae6c8e82ef5..0729a0a4ffb8 100644
--- 
a/core/camel-console/src/main/java/org/apache/camel/impl/console/SendDevConsole.java
+++ 
b/core/camel-console/src/main/java/org/apache/camel/impl/console/SendDevConsole.java
@@ -22,7 +22,12 @@ import java.util.Base64;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
+import org.apache.camel.CamelExchangeException;
 import org.apache.camel.ConsumerTemplate;
 import org.apache.camel.Endpoint;
 import org.apache.camel.Exchange;
@@ -39,6 +44,7 @@ import org.apache.camel.support.MessageHelper;
 import org.apache.camel.support.console.AbstractDevConsole;
 import org.apache.camel.support.service.ServiceHelper;
 import org.apache.camel.util.StopWatch;
+import org.apache.camel.util.StringHelper;
 import org.apache.camel.util.TimeUtils;
 import org.apache.camel.util.json.JsonRecordSupport;
 
@@ -64,7 +70,7 @@ public class SendDevConsole extends AbstractDevConsole {
     private int bodyMaxChars = 32 * 1024;
 
     @Metadata(defaultValue = "20000", label = "advanced",
-              description = "Timeout when using poll mode")
+              description = "Timeout of the send, or of the poll in poll mode, 
in millis")
     private int pollTimeout = 20000;
 
     @Metadata(label = "query", description = "Maximum size of the message body 
to include in the dump",
@@ -84,7 +90,7 @@ public class SendDevConsole extends AbstractDevConsole {
               javaType = "java.lang.Boolean")
     public static final String POLL = "poll";
 
-    @Metadata(label = "query", description = "Timeout when using poll mode",
+    @Metadata(label = "query", description = "Timeout of the send, or of the 
poll in poll mode, in millis",
               javaType = "java.lang.Integer", defaultValue = "20000")
     public static final String POLL_TIMEOUT = "pollTimeout";
 
@@ -141,9 +147,9 @@ public class SendDevConsole extends AbstractDevConsole {
         }
         String exchangePattern = optionString(options, EXCHANGE_PATTERN);
         boolean poll = optionBoolean(options, POLL, false);
+        // bounds the send as well as the poll: a send to direct:nope waited 
for the 30 seconds of direct, longer than
+        // the CLI waits for the answer (CAMEL-26929)
         int timeout = optionInt(options, POLL_TIMEOUT, pollTimeout);
-        // give extra time as CLI needs to process reply also
-        timeout += 5000;
 
         Endpoint target = null;
         Exchange out = null;
@@ -162,7 +168,9 @@ public class SendDevConsole extends AbstractDevConsole {
         }
         long taken = watch.taken();
         String status = "success";
-        if (cause != null) {
+        if (cause instanceof SendTimeoutException) {
+            status = "timeout";
+        } else if (cause != null) {
             status = "error";
         } else if (poll && out == null) {
             status = "timeout";
@@ -206,9 +214,9 @@ public class SendDevConsole extends AbstractDevConsole {
         }
         String exchangePattern = optionString(options, EXCHANGE_PATTERN);
         boolean poll = optionBoolean(options, POLL, false);
+        // bounds the send as well as the poll: a send to direct:nope waited 
for the 30 seconds of direct, longer than
+        // the CLI waits for the answer (CAMEL-26929)
         int timeout = optionInt(options, POLL_TIMEOUT, pollTimeout);
-        // give extra time as CLI needs to process reply also
-        timeout += 5000;
 
         Endpoint target = null;
         Exchange out = null;
@@ -227,7 +235,9 @@ public class SendDevConsole extends AbstractDevConsole {
         }
         long taken = watch.taken();
         String status = "success";
-        if (cause != null) {
+        if (cause instanceof SendTimeoutException) {
+            status = "timeout";
+        } else if (cause != null) {
             status = "error";
         } else if (poll && out == null) {
             status = "timeout";
@@ -270,20 +280,63 @@ public class SendDevConsole extends AbstractDevConsole {
             if (poll) {
                 out = consumer.receive(target, timeout);
             } else {
-                final String mep = exchangePattern;
-                out = producer.send(target, exchange -> {
-                    exchange.getMessage().setBody(inputBody);
-                    if (!inputHeaders.isEmpty()) {
-                        exchange.getMessage().setHeaders(inputHeaders);
-                    }
-                    exchange.setPattern(
-                            "InOut".equals(mep) ? ExchangePattern.InOut : 
ExchangePattern.InOnly);
-                });
+                Exchange exchange = target.createExchange(
+                        "InOut".equals(exchangePattern) ? 
ExchangePattern.InOut : ExchangePattern.InOnly);
+                exchange.getMessage().setBody(inputBody);
+                if (!inputHeaders.isEmpty()) {
+                    exchange.getMessage().setHeaders(inputHeaders);
+                }
+                if (isDirectWithoutConsumer(target)) {
+                    // direct waits 30 seconds for a consumer by default: no 
route will start to consume it now
+                    throw new CamelExchangeException("No consumers available 
on endpoint: " + target, exchange);
+                }
+                CompletableFuture<Exchange> future = 
producer.asyncSend(target, exchange);
+                try {
+                    out = future.get(timeout, TimeUnit.MILLISECONDS);
+                } catch (TimeoutException e) {
+                    // the exchange goes on in the integration, but the caller 
has its answer
+                    throw new SendTimeoutException(exchange, timeout);
+                } catch (ExecutionException e) {
+                    throw e.getCause() instanceof Exception ex ? ex : e;
+                } catch (InterruptedException e) {
+                    // keep the interrupt for the caller, such as a console 
call that is stopped
+                    Thread.currentThread().interrupt();
+                    throw new CamelExchangeException("The send was 
interrupted", exchange, e);
+                }
             }
         }
         return out;
     }
 
+    /**
+     * Whether the endpoint is a direct endpoint that no route consumes from.
+     */
+    private boolean isDirectWithoutConsumer(Endpoint target) {
+        String key = target.getEndpointKey();
+        if (key == null || !key.startsWith("direct:")) {
+            return false;
+        }
+        String path = StringHelper.before(key, "?", key);
+        for (Route route : getCamelContext().getRoutes()) {
+            String consumer = route.getEndpoint().getEndpointKey();
+            if (consumer != null && path.equals(StringHelper.before(consumer, 
"?", consumer))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * The send did not complete within the timeout.
+     */
+    private static final class SendTimeoutException extends 
CamelExchangeException {
+
+        SendTimeoutException(Exchange exchange, long timeout) {
+            super("The send did not complete within " + timeout + " millis 
(the exchange goes on in the integration)",
+                  exchange);
+        }
+    }
+
     /**
      * Why nothing was sent: no endpoint matches, or, without an endpoint, 
there is no route to send to (before, that
      * was reported as sent).
diff --git 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/SendDevConsoleTest.java
 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/SendDevConsoleTest.java
index 331046d4bbda..a640add52233 100644
--- 
a/core/camel-console/src/test/java/org/apache/camel/impl/console/SendDevConsoleTest.java
+++ 
b/core/camel-console/src/test/java/org/apache/camel/impl/console/SendDevConsoleTest.java
@@ -54,6 +54,9 @@ public class SendDevConsoleTest extends 
AbstractDevConsoleTest {
             public void configure() {
                 from("direct:start").routeId("myRoute")
                         .to("mock:result");
+                from("direct:slow").routeId("slowRoute")
+                        .delay(5000)
+                        .to("mock:slow");
             }
         };
     }
@@ -72,6 +75,85 @@ public class SendDevConsoleTest extends 
AbstractDevConsoleTest {
         assertNotNull(jsonOut.getString("endpoint"));
     }
 
+    @Test
+    public void testSendIsBoundedByTheTimeout() {
+        // the timeout was for --poll only: the send waited as long as the 
route took (CAMEL-26929)
+        DevConsole console = startedSendConsole();
+
+        Map<String, Object> options = createOptions("direct:slow", "Hello");
+        options.put(SendDevConsole.POLL_TIMEOUT, "200");
+        JsonObject out = callJson(console, options);
+
+        assertEquals("timeout", out.getString("status"), out.toJson());
+        // well under the 5 seconds of the route, with room for a slow CI 
machine
+        assertTrue(out.getLong("elapsed") < 4000, "answered at the timeout: " 
+ out.toJson());
+        JsonObject exception = map(out, "exception");
+        assertNotNull(exception, out.toJson());
+        assertTrue(exception.getString("message").startsWith("The send did not 
complete within 200 millis"),
+                exception.toJson());
+    }
+
+    @Test
+    public void testAnInterruptedSendKeepsTheInterrupt() {
+        DevConsole console = startedSendConsole();
+
+        Map<String, Object> options = createOptions("direct:slow", "Hello");
+        options.put(SendDevConsole.POLL_TIMEOUT, "20000");
+        Thread.currentThread().interrupt();
+        JsonObject out;
+        boolean interrupted;
+        try {
+            out = callJson(console, options);
+        } finally {
+            // clears the flag for the tests after this one
+            interrupted = Thread.interrupted();
+        }
+
+        assertTrue(interrupted, "the interrupt is kept for the caller");
+        assertEquals("error", out.getString("status"), out.toJson());
+        assertTrue(map(out, "exception").getString("message").startsWith("The 
send was interrupted"), out.toJson());
+    }
+
+    @Test
+    public void testSendCompletesWithinTheTimeout() {
+        DevConsole console = startedSendConsole();
+
+        Map<String, Object> options = createOptions("direct:start", "Hello");
+        options.put(SendDevConsole.EXCHANGE_PATTERN, "InOut");
+        options.put(SendDevConsole.POLL_TIMEOUT, "5000");
+        JsonObject out = callJson(console, options);
+
+        assertEquals("success", out.getString("status"), out.toJson());
+        assertEquals("Hello", map(map(out, "message"), 
"body").getString("value"), out.toJson());
+    }
+
+    @Test
+    public void testSendToDirectWithoutConsumerFailsAtOnce() {
+        // direct waits 30 seconds for a consumer by default, longer than the 
CLI waits for the answer (CAMEL-26929)
+        DevConsole console = startedSendConsole();
+
+        Map<String, Object> options = createOptions("direct:nope", "Hello");
+        JsonObject out = callJson(console, options);
+
+        assertEquals("error", out.getString("status"), out.toJson());
+        // well under the 30 seconds direct waits
+        assertTrue(out.getLong("elapsed") < 10000, out.toJson());
+        assertTrue(map(out, "exception").getString("message").startsWith("No 
consumers available on endpoint: direct://nope"),
+                out.toJson());
+
+        // a direct endpoint with options of its own is the endpoint of its 
route
+        options = createOptions("direct:start?timeout=1000", "Hello");
+        assertEquals("success", callJson(console, 
options).getString("status"));
+    }
+
+    private static JsonObject map(JsonObject jo, String key) {
+        Map<?, ?> map = (Map<?, ?>) jo.get(key);
+        assertNotNull(map, key + " in " + jo.toJson());
+        JsonObject answer = new JsonObject();
+        map.forEach((k, v) -> answer.put(String.valueOf(k), v));
+        return answer;
+    }
+
     @Test
     public void testSendConsoleInOutPattern() {
         DevConsole console = assertConsoleExists("send");
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 105b6ed66006..f8d465a3ff3c 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -2821,6 +2821,10 @@ A script that checks the exit code of a command must 
take these changes into acc
   cannot be generated and `70` for an unexpected error, instead of `4`, `2`, 
`3` and `4`.
 * `camel transform dataweave` exits with `2` without `--input` or 
`--expression`, and with `66` when the input path does
   not exist or holds no `.dwl` files, instead of `1`.
+* `camel cmd send --timeout` bounds the send in the integration: a send that 
does not complete in time is `Timeout`
+  with exit `124`, and the exchange goes on in the integration. Before, it 
only bounded how long the CLI waited, and a
+  timeout below 5 seconds was raised to 5 seconds. A send to a `direct` 
endpoint that no route consumes fails at once
+  with "No consumers available" and exit `1`, instead of waiting 30 seconds 
for a consumer.
 * `camel cmd sql` exits with `124` instead of `1` when the query does not 
answer in time.
 * A source file of `camel cmd route-diagram`, `camel cmd route-structure` and 
`camel cmd route-topology` that does not
   exist exits with `66` instead of `1`. The parts of a Java route the parser 
could not read are printed as a warning on
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
index 031bc12acdbf..94e77bb6c816 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendAction.java
@@ -192,9 +192,10 @@ public class CamelSendAction extends ActionBaseCommand {
                 }
             }
         }
-        // timeout cannot be too low
-        if (timeout < 5000) {
-            timeout = 5000;
+        // the integration bounds the send by the timeout as given: a timeout 
below 5 seconds was raised to 5 seconds
+        // without a word, while the send itself was not bounded at all 
(CAMEL-26929)
+        if (timeout <= 0) {
+            throw CliException.usage("--timeout must be more than 0, was: " + 
timeout);
         }
         if (repeat < 0 || repeat > MAX_MESSAGES) {
             throw CliException.usage("--repeat must be between 1 and " + 
MAX_MESSAGES + ", was: " + repeat);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionResultTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionResultTest.java
index 3e2ca538658e..3f30a46e1dd2 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionResultTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSendActionResultTest.java
@@ -58,14 +58,31 @@ class CamelSendActionResultTest extends 
ActionCommandTestSupport {
         assertThat(action.getString("endpoint")).isEqualTo("direct:start");
         assertThat(action.getString("body")).isEqualTo("hello");
         assertThat(action.getString("exchangePattern")).isEqualTo("InOnly");
-        // the timeout is at least 5 seconds
-        assertThat(action.getLong("pollTimeout")).isEqualTo(5000L);
+        // the timeout as given: it was raised to 5 seconds without a word, 
and the integration bounds the send by it
+        // (CAMEL-26929)
+        assertThat(action.getLong("pollTimeout")).isEqualTo(100L);
         JsonArray headers = action.getCollection("headers");
         JsonObject header = headers.getMap(0);
         assertThat(header.getString("key")).isEqualTo("id");
         assertThat(header.getString("value")).isEqualTo("1");
     }
 
+    @Test
+    void aTimeoutOfZeroIsAUsageError() throws Exception {
+        writeStatusFile(TEST_PID, "orders");
+
+        assertThatThrownBy(() -> callWithResponse(command("orders", 
"--endpoint=direct:start", "--timeout=0"),
+                answer("success")))
+                .isInstanceOfSatisfying(CliException.class,
+                        e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+                .hasMessage("--timeout must be more than 0, was: 0");
+        assertThatThrownBy(() -> callWithResponse(command("orders", 
"--endpoint=direct:start", "--timeout=-1"),
+                answer("success")))
+                .isInstanceOfSatisfying(CliException.class,
+                        e -> 
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+                .hasMessage("--timeout must be more than 0, was: -1");
+    }
+
     @Test
     void failedAndTimedOutExchanges() throws Exception {
         writeStatusFile(TEST_PID, "orders");

Reply via email to