This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26427-cli-exit-wait in repository https://gitbox.apache.org/repos/asf/camel.git
commit a0f1458713107fa2d95fe9c2c354ebc9440d3210 Author: Claus Ibsen <[email protected]> AuthorDate: Sat Oct 31 07:48:32 2026 +0100 CAMEL-26427: camel-jbang - Every CLI command waits 300 ms before the JVM exits Harden created its HTTP client in a field initializer. The CLI creates all its commands at startup, so every run started the selector thread of the client, and the JVM waits up to 300 ms for a thread in native code when it exits. The client is now created on first use. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/dsl/jbang/core/commands/Harden.java | 21 +++++++++++++++------ .../dsl/jbang/core/commands/CamelJBangMainTest.java | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java index 2ad81ad4358b..63461f1e5382 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Harden.java @@ -197,9 +197,18 @@ public class Harden extends CamelCommand { // whether the answer for the current file was printed as it came (reset per file) private boolean streamed; - private final HttpClient httpClient = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(CONNECT_TIMEOUT_SECONDS)) - .build(); + // created on first use: the CLI creates all its commands at startup, and the selector thread of a client makes the + // JVM wait up to 300 ms when it exits + private HttpClient httpClient; + + private HttpClient httpClient() { + if (httpClient == null) { + httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(CONNECT_TIMEOUT_SECONDS)) + .build(); + } + return httpClient; + } public Harden(CamelJBangMain main) { super(main); @@ -370,7 +379,7 @@ public class Harden extends CamelCommand { .timeout(Duration.ofSeconds(HEALTH_CHECK_TIMEOUT_SECONDS)) .GET() .build(); - HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse<String> response = httpClient().send(request, HttpResponse.BodyHandlers.ofString()); return response.statusCode() == 200; } catch (Exception e) { return false; @@ -628,7 +637,7 @@ public class Harden extends CamelCommand { .POST(HttpRequest.BodyPublishers.ofString(body.toJson())) .build(); - HttpResponse<Stream<String>> response = httpClient.send( + HttpResponse<Stream<String>> response = httpClient().send( request, HttpResponse.BodyHandlers.ofLines()); if (response.statusCode() != 200) { @@ -678,7 +687,7 @@ public class Harden extends CamelCommand { builder.header("Authorization", "Bearer " + authKey); } - HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + HttpResponse<String> response = httpClient().send(builder.build(), HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == 200) { return (JsonObject) Jsoner.deserialize(response.body()); diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMainTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMainTest.java index f017fadbbd10..ab1e9c1b3060 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMainTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMainTest.java @@ -18,12 +18,14 @@ package org.apache.camel.dsl.jbang.core.commands; import java.lang.reflect.Field; import java.util.List; +import java.util.Set; import org.apache.camel.dsl.jbang.core.common.PluginHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import picocli.CommandLine; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -66,4 +68,20 @@ class CamelJBangMainTest extends CamelCommandBaseTestSupport { List<String> output = printer.getLines(); assertFalse(output.stream().anyMatch(l -> l.contains("Plugins (not installed):"))); } + + @Test + public void creatingTheCommandsStartsNoThread() { + // the CLI creates all its commands at startup: a thread that a command starts when it is created is in every + // run, and a thread in native code when the JVM exits (such as the selector of an HTTP client) makes every + // command wait up to 300 ms before the JVM exits + Set<Thread> before = Thread.getAllStackTraces().keySet(); + + main.newCommandLine(); + + List<String> started = Thread.getAllStackTraces().keySet().stream() + .filter(t -> !before.contains(t) && t.isAlive()) + .map(Thread::getName) + .toList(); + assertEquals(List.of(), started, "Threads started by creating the commands of the CLI"); + } }
