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 8bdcd36755e7 CAMEL-25438: camel-jbang - monitor tabs do not wait
behind data loads of hidden tabs (#27544)
8bdcd36755e7 is described below
commit 8bdcd36755e7043ff53ec537c0429bf831a087ee
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 8 09:41:09 2026 +0200
CAMEL-25438: camel-jbang - monitor tabs do not wait behind data loads of
hidden tabs (#27544)
An integration answers one action at a time, about one a second, and
selecting an integration sent four actions for tabs that were not shown:
the classpath (jvm) for Classpath, CVE Audit and Maven Dependencies each,
and the route structure for Catalog. The tab the user opened waited
behind them, 2-5 s, and longer when switching integrations.
These tabs now load when shown, as onTabSelected already did; the shown
More tab loads again after an integration change. The tabs that need the
classpath share one jvm answer per integration for 30 s.
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../dsl/jbang/core/commands/tui/CatalogTab.java | 5 +-
.../dsl/jbang/core/commands/tui/ClasspathTab.java | 10 +-
.../dsl/jbang/core/commands/tui/CveAuditTab.java | 13 +--
.../jbang/core/commands/tui/HeapHistogramTab.java | 5 +-
.../core/commands/tui/MavenDependenciesTab.java | 5 +-
.../jbang/core/commands/tui/MonitorContext.java | 30 ++++++
.../dsl/jbang/core/commands/tui/TabRegistry.java | 4 +
.../core/commands/tui/MonitorTabLazyLoadTest.java | 92 ++++++++++++++++++
.../dsl/jbang/core/commands/tui/NoopBackend.java | 104 +++++++++++++++++++++
.../tui/TuiBackendHelperRecordingTest.java | 83 ----------------
10 files changed, 238 insertions(+), 113 deletions(-)
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
index 2b71875c041b..aa95009e975f 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CatalogTab.java
@@ -120,10 +120,7 @@ class CatalogTab extends AbstractTableTab {
errorMessage = null;
dataLoaded = false;
loading.set(false);
- if (ctx.selectedPid != null) {
- lastPid = ctx.selectedPid;
- loadCatalogData();
- }
+ // loaded when the tab is shown (onTabSelected): a hidden tab must not
hold up the one the user looks at
}
boolean isFilterInputActive() {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
index d517531efae8..f39604e02e61 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ClasspathTab.java
@@ -111,10 +111,7 @@ class ClasspathTab extends AbstractTab {
errorMessage = null;
dataLoaded = false;
loading.set(false);
- if (ctx.selectedPid != null) {
- lastPid = ctx.selectedPid;
- loadClasspath();
- }
+ // loaded when the tab is shown (onTabSelected): a hidden tab must not
hold up the one the user looks at
}
@Override
@@ -318,10 +315,7 @@ class ClasspathTab extends AbstractTab {
String pid = ctx.selectedPid;
ctx.backgroundExecutor.execute(() -> {
try {
- JsonObject action = new JsonObject();
- action.put("action", "jvm");
-
- JsonObject response = ctx.executeAction(pid, action, 5000);
+ JsonObject response = ctx.jvmInfo(pid, 5000);
if (response == null) {
applyResult(Collections.emptyList(), "No response from
integration");
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
index 3186fad5b0d3..5f2f1f33f20b 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/CveAuditTab.java
@@ -124,10 +124,7 @@ class CveAuditTab extends AbstractTableTab {
dataLoaded = false;
detailScroll = 0;
loading.set(false);
- if (ctx.selectedPid != null) {
- lastPid = ctx.selectedPid;
- loadAndScan();
- }
+ // loaded when the tab is shown (onTabSelected): a hidden tab must not
hold up the one the user looks at
}
@Override
@@ -488,9 +485,7 @@ class CveAuditTab extends AbstractTableTab {
Map<String, Path> classpathJars = new HashMap<>();
String pid = ctx.selectedPid;
if (pid != null) {
- JsonObject action = new JsonObject();
- action.put("action", "jvm");
- JsonObject response = ctx.executeAction(pid, action, 5000);
+ JsonObject response = ctx.jvmInfo(pid, 5000);
if (response != null) {
Object cp = response.get("classpath");
if (cp instanceof JsonArray arr) {
@@ -558,9 +553,7 @@ class CveAuditTab extends AbstractTableTab {
if (pid == null) {
return Collections.emptyList();
}
- JsonObject action = new JsonObject();
- action.put("action", "jvm");
- JsonObject response = ctx.executeAction(pid, action, 5000);
+ JsonObject response = ctx.jvmInfo(pid, 5000);
if (response == null) {
return Collections.emptyList();
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
index eb066ebab35c..47c792d58f94 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/HeapHistogramTab.java
@@ -479,10 +479,7 @@ class HeapHistogramTab extends AbstractTableTab {
}
private List<ClasspathTab.JarEntry> loadClasspathEntries(String pid) {
- JsonObject action = new JsonObject();
- action.put("action", "jvm");
-
- JsonObject response = ctx.executeAction(pid, action, 5000);
+ JsonObject response = ctx.jvmInfo(pid, 5000);
if (response == null) {
return Collections.emptyList();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
index d834970d7ef8..fe6b7097846e 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MavenDependenciesTab.java
@@ -122,10 +122,7 @@ class MavenDependenciesTab extends AbstractTableTab {
transitiveLoaded = false;
transitiveEntries = Collections.emptyList();
loading.set(false);
- if (ctx.selectedPid != null) {
- lastPid = ctx.selectedPid;
- loadDependencies();
- }
+ // loaded when the tab is shown (onTabSelected): a hidden tab must not
hold up the one the user looks at
}
@Override
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
index f966d66ae5c1..46e566a6b693 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java
@@ -184,6 +184,36 @@ class MonitorContext {
}
}
+ /** How long the jvm answer of an integration is kept for the tabs that
need its classpath. */
+ static final long JVM_INFO_TTL_MS = 30_000;
+ private final ConcurrentHashMap<String, CachedAnswer> jvmInfo = new
ConcurrentHashMap<>();
+
+ private record CachedAnswer(JsonObject answer, long time) {
+ }
+
+ /**
+ * The jvm answer of an integration (its classpath and JVM details),
shared by the tabs that need it (Classpath, CVE
+ * Audit, Maven Dependencies, Heap Histogram): an action takes a second of
the integration, so it is asked once, not
+ * once per tab.
+ */
+ JsonObject jvmInfo(String pid, long timeoutMs) {
+ Object lock = actionLocks.computeIfAbsent(pid, k -> new Object());
+ synchronized (lock) {
+ CachedAnswer cached = jvmInfo.get(pid);
+ long now = System.currentTimeMillis();
+ if (cached != null && now - cached.time() < JVM_INFO_TTL_MS) {
+ return cached.answer();
+ }
+ JsonObject request = new JsonObject();
+ request.put("action", "jvm");
+ JsonObject answer = executeAction(pid, request, timeoutMs);
+ if (answer != null) {
+ jvmInfo.put(pid, new CachedAnswer(answer, now));
+ }
+ return answer;
+ }
+ }
+
JsonObject executeAction(String pid, JsonObject request, long timeoutMs) {
Object lock = actionLocks.computeIfAbsent(pid, k -> new Object());
synchronized (lock) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
index 3cfc2b5221f5..82963bfe0f75 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/TabRegistry.java
@@ -379,6 +379,10 @@ class TabRegistry {
for (MoreTab mt : moreTabs) {
mt.tab().onIntegrationChanged();
}
+ // the tabs load their data when shown: the one on screen loads now,
for the new integration
+ if (activeMoreTab != null && tabsState.selected() == TAB_MORE) {
+ activeMoreTab.onTabSelected();
+ }
dataService.otelSpans().set(List.of());
filesBrowser.reset();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTabLazyLoadTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTabLazyLoadTest.java
new file mode 100644
index 000000000000..5efb1f081b58
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorTabLazyLoadTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.dsl.jbang.core.commands.tui;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.tui.TuiRunner;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25438: the integration answers one action at a time, so a tab that is
not shown must not ask it for data when
+ * the selected integration changes, and the tabs that need the classpath
share one answer.
+ */
+// a TUI runner is created: not alongside the other tests that create one
+@Isolated
+class MonitorTabLazyLoadTest {
+
+ @Test
+ void hiddenTabsDoNotLoadWhenTheIntegrationChanges() throws Exception {
+ CountingContext ctx = new CountingContext();
+ try (TuiRunner runner = TuiBackendHelper.createTuiRunner(new
NoopBackend())) {
+ ctx.runner = runner;
+ ctx.selectedPid = "1234";
+ List<MonitorTab> tabs = List.of(new ClasspathTab(ctx), new
CveAuditTab(ctx), new MavenDependenciesTab(ctx),
+ new CatalogTab(ctx));
+
+ tabs.forEach(MonitorTab::onIntegrationChanged);
+ Thread.sleep(500);
+ assertThat(ctx.actions).isEmpty();
+
+ // shown: the tab loads
+ tabs.get(0).onTabSelected();
+ long deadline = System.currentTimeMillis() + 5000;
+ while (ctx.actions.isEmpty() && System.currentTimeMillis() <
deadline) {
+ Thread.sleep(20);
+ }
+ assertThat(ctx.actions).containsExactly("jvm");
+ }
+ }
+
+ @Test
+ void theTabsShareTheJvmAnswer() {
+ CountingContext ctx = new CountingContext();
+
+ JsonObject first = ctx.jvmInfo("1234", 5000);
+ JsonObject second = ctx.jvmInfo("1234", 5000);
+
+ assertThat(second).isSameAs(first);
+ assertThat(ctx.actions).containsExactly("jvm");
+ // another integration has its own
+ ctx.jvmInfo("5678", 5000);
+ assertThat(ctx.actions).containsExactly("jvm", "jvm");
+ }
+
+ private static final class CountingContext extends MonitorContext {
+
+ final List<String> actions = new CopyOnWriteArrayList<>();
+
+ CountingContext() {
+ super(new AtomicReference<>(List.of()), new
AtomicReference<>(List.of()));
+ }
+
+ @Override
+ JsonObject executeAction(String pid, JsonObject request, long
timeoutMs) {
+ actions.add(request.getString("action"));
+ JsonObject answer = new JsonObject();
+ answer.put("classpath", new JsonArray());
+ return answer;
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/NoopBackend.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/NoopBackend.java
new file mode 100644
index 000000000000..de98fa454b6a
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/NoopBackend.java
@@ -0,0 +1,104 @@
+/*
+ * 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.dsl.jbang.core.commands.tui;
+
+import java.io.IOException;
+
+import dev.tamboui.buffer.DiffResult;
+import dev.tamboui.layout.Position;
+import dev.tamboui.layout.Size;
+import dev.tamboui.terminal.Backend;
+
+/**
+ * Minimal {@link Backend} stand-in. Only {@link #size()} needs a meaningful
value, so that the test fails if the
+ * recording dimensions are taken from the delegate instead of the config.
+ */
+final class NoopBackend implements Backend {
+
+ @Override
+ public void draw(DiffResult diff) throws IOException {
+ }
+
+ @Override
+ public void flush() throws IOException {
+ }
+
+ @Override
+ public void clear() throws IOException {
+ }
+
+ @Override
+ public Size size() throws IOException {
+ return new Size(80, 24);
+ }
+
+ @Override
+ public void showCursor() throws IOException {
+ }
+
+ @Override
+ public void hideCursor() throws IOException {
+ }
+
+ @Override
+ public Position getCursorPosition() throws IOException {
+ return new Position(0, 0);
+ }
+
+ @Override
+ public void setCursorPosition(Position position) throws IOException {
+ }
+
+ @Override
+ public void enterAlternateScreen() throws IOException {
+ }
+
+ @Override
+ public void leaveAlternateScreen() throws IOException {
+ }
+
+ @Override
+ public void enableRawMode() throws IOException {
+ }
+
+ @Override
+ public void disableRawMode() throws IOException {
+ }
+
+ @Override
+ public void onResize(Runnable handler) {
+ }
+
+ @Override
+ public int read(int timeoutMs) throws IOException {
+ return -2;
+ }
+
+ @Override
+ public int peek(int timeoutMs) throws IOException {
+ return -2;
+ }
+
+ @Override
+ public void writeRaw(byte[] data) throws IOException {
+ // TuiRunner.create enables bracketed paste through this method
+ }
+
+ @Override
+ public void close() throws IOException {
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
index c95f2a274b38..0f446adf9ac7 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiBackendHelperRecordingTest.java
@@ -16,13 +16,10 @@
*/
package org.apache.camel.dsl.jbang.core.commands.tui;
-import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
-import dev.tamboui.buffer.DiffResult;
import dev.tamboui.internal.record.AnsiTerminalCapture;
-import dev.tamboui.layout.Position;
import dev.tamboui.layout.Size;
import dev.tamboui.terminal.Backend;
import dev.tamboui.tui.TuiRunner;
@@ -92,84 +89,4 @@ class TuiBackendHelperRecordingTest {
assertThat(result.size()).isEqualTo(new Size(120, 30));
}
}
-
- /**
- * Minimal {@link Backend} stand-in. Only {@link #size()} needs a
meaningful value, so that the test fails if the
- * recording dimensions are taken from the delegate instead of the config.
- */
- private static final class NoopBackend implements Backend {
-
- @Override
- public void draw(DiffResult diff) throws IOException {
- }
-
- @Override
- public void flush() throws IOException {
- }
-
- @Override
- public void clear() throws IOException {
- }
-
- @Override
- public Size size() throws IOException {
- return new Size(80, 24);
- }
-
- @Override
- public void showCursor() throws IOException {
- }
-
- @Override
- public void hideCursor() throws IOException {
- }
-
- @Override
- public Position getCursorPosition() throws IOException {
- return new Position(0, 0);
- }
-
- @Override
- public void setCursorPosition(Position position) throws IOException {
- }
-
- @Override
- public void enterAlternateScreen() throws IOException {
- }
-
- @Override
- public void leaveAlternateScreen() throws IOException {
- }
-
- @Override
- public void enableRawMode() throws IOException {
- }
-
- @Override
- public void disableRawMode() throws IOException {
- }
-
- @Override
- public void onResize(Runnable handler) {
- }
-
- @Override
- public int read(int timeoutMs) throws IOException {
- return -2;
- }
-
- @Override
- public int peek(int timeoutMs) throws IOException {
- return -2;
- }
-
- @Override
- public void writeRaw(byte[] data) throws IOException {
- // TuiRunner.create enables bracketed paste through this method
- }
-
- @Override
- public void close() throws IOException {
- }
- }
}