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 699f0798f7dc chore: camel-jbang - an integration that wrote no status
for 15 seconds says so, instead of running at its last rate (#29524)
699f0798f7dc is described below
commit 699f0798f7dc3f207722ddec2576364b4e2d773d
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 10 07:46:44 2026 +0100
chore: camel-jbang - an integration that wrote no status for 15 seconds
says so, instead of running at its last rate (#29524)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../ROOT/pages/camel-jbang-tui-observe.adoc | 4 +
.../dsl/jbang/core/commands/tui/AboutView.java | 26 ++--
.../core/commands/tui/DataRefreshService.java | 33 +++--
.../jbang/core/commands/tui/IntegrationInfo.java | 7 +
.../dsl/jbang/core/commands/tui/McpFacade.java | 6 +-
.../dsl/jbang/core/commands/tui/OverviewTab.java | 17 +--
.../dsl/jbang/core/commands/tui/StatusAge.java | 141 ++++++++++++++++++++
.../src/main/resources/tui/help/overview.md | 8 +-
.../dsl/jbang/core/commands/tui/AboutViewTest.java | 19 +++
.../commands/tui/DataRefreshMembershipTest.java | 20 ++-
.../dsl/jbang/core/commands/tui/StatusAgeTest.java | 145 +++++++++++++++++++++
11 files changed, 395 insertions(+), 31 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
index ab7c0d28f6f1..98e4bc0533df 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-observe.adoc
@@ -36,6 +36,10 @@ over the last 5 minutes, coloured as on the Timeline, with
when the worst happen
allows, and on a tall screen up to two rows high, with room around the panels.
*+* and *-* zoom between 5, 2 and 1
minutes (zooming in centres on the last trouble, so you see what the other
routes did then), and *Left* and *Right*
move a zoomed window. *4* opens the Timeline for each second.
+* *Watch* has the errors to look at. An integration that wrote no status for
15 seconds, while its process runs, says
+since when instead: its numbers are of then. Its CPU tells why: busy when its
heap is full and the garbage collector runs
+all the time, idle when a debugger holds it. A stop may not reach it, and
*F10* kills it. The Overview table shows it
+as `No status`.
`camel get about` shows the same from the command line (see
xref:camel-jbang-diagnostics.adoc#_about[Diagnostics]).
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
index 9d83e720e76d..630723f681ac 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
@@ -596,8 +596,9 @@ final class AboutView {
String p = purpose;
String pFrom = purposeFrom;
List<Line> head = new ArrayList<>();
- head.add(Line.from(Span.raw(" "), Span.raw(meta(info, o)),
- Span.styled(info.vanishing ? " ○ stopped" : " ● running",
+ head.add(Line.from(Span.raw(" "), Span.raw(meta(info, o)),
StatusAge.stale(info)
+ ? Span.styled(" ✗ no status", Theme.error())
+ : Span.styled(info.vanishing ? " ○ stopped" : " ● running",
info.vanishing ? Theme.muted() : Theme.success())));
if (p != null) {
for (String l : wrap(p, Math.max(20, width - 8), 2)) {
@@ -959,7 +960,7 @@ final class AboutView {
// the exchanges of the route it starts over the last minutes, and
the rate now
long[] perSecond = e.route() != null ? routePerSecond(info,
e.route()) : null;
if (perSecond != null) {
- in.add(sparkLine(" ", perSecond, spark));
+ in.add(sparkLine(" ", perSecond, spark,
StatusAge.stale(info)));
inTargets.add(null);
}
}
@@ -1004,7 +1005,7 @@ final class AboutView {
if (perSecond != null) {
// the sparkline in what " from <route> " and the rate
leave of the column
int room = outWidth - 9 - s.route().length() - 2 - 8;
- from.addAll(sparkLine(" ", perSecond, Math.max(4,
Math.min(spark, room)))
+ from.addAll(sparkLine(" ", perSecond, Math.max(4,
Math.min(spark, room)), StatusAge.stale(info))
.spans());
}
out.add(Line.from(from));
@@ -1036,13 +1037,16 @@ final class AboutView {
return null;
}
- /** The values as a sparkline and the rate now, or only idle when there
was nothing. */
- private static Line sparkLine(String indent, long[] values, int width) {
+ /**
+ * The values as a sparkline and the rate now, or only idle when there was
nothing; the rate is not known when the
+ * integration wrote no status of late.
+ */
+ private static Line sparkLine(String indent, long[] values, int width,
boolean noStatus) {
if (Arrays.stream(values).allMatch(v -> v <= 0)) {
- return Line.from(Span.raw(indent), Span.styled("idle",
Theme.muted()));
+ return Line.from(Span.raw(indent), Span.styled(noStatus ? "-" :
"idle", Theme.muted()));
}
return Line.from(Span.raw(indent), Span.styled(spark(values, width),
Theme.info()),
- Span.styled(" " + rate(values), Theme.muted()));
+ Span.styled(" " + (noStatus ? "-" : rate(values)),
Theme.muted()));
}
/** The sparklines of In fill its column, between {@value #SPARK_WIDTH}
and {@value #MAX_SPARK_WIDTH} wide. */
@@ -1118,7 +1122,11 @@ final class AboutView {
lines.add(happened(info, width));
List<Span> watch = new ArrayList<>();
watch.add(Span.styled(" Watch ", Theme.muted()));
- if (info.errorCount > 0) {
+ List<Span> noStatus = StatusAge.watch(info);
+ if (!noStatus.isEmpty()) {
+ // its numbers are of when it stopped writing its status
+ watch.addAll(noStatus);
+ } else if (info.errorCount > 0) {
// errors that stopped a minute ago are history: the line says so,
and calms down. A minute, not the 5 of
// Last 5m and the strips (which still show them): a fix that
reloads should not stay red for long
long lastError = lastError(info);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
index a1a8f13fe86b..8c2910d0c956 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshService.java
@@ -323,9 +323,12 @@ class DataRefreshService {
continue;
}
IntegrationInfo info = StatusParser.parseIntegration(ph, root);
+ if (info != null) {
+ info.statusWrittenAt = writtenAt(pid);
+ }
if (info != null && ctx != null) {
ctx.chaosMarks.record(info);
- ctx.timeline.record(info, writtenAt(pid));
+ ctx.timeline.record(info, info.statusWrittenAt);
mergeTimelineFile(pid);
timelineLog.read(info.pid, logFile(info), ctx.timeline);
}
@@ -334,16 +337,10 @@ class DataRefreshService {
if (prev != null) {
info.errors = prev.errors;
}
+ StatusAge.observeCpu(info, prev, ph, now);
rates.fill(info, now);
infos.add(info);
- metrics.updateThroughputHistory(info);
- metrics.updateEndpointHistory(info);
- metrics.updateServiceHistory(info);
- metrics.updateCbHistory(info);
- metrics.updateHeapHistory(info);
- if (ctx != null) {
- ctx.healthHistory.record(info,
System.currentTimeMillis() / 1000);
- }
+ recordHistories(info);
metrics.updateLoadMetrics(ph, info);
detectReload(info);
}
@@ -911,6 +908,24 @@ class DataRefreshService {
return msg;
}
+ /**
+ * The charts and strips of the integration go on by a second: from a
status of late only, as one of then would draw
+ * its last rate on and on.
+ */
+ void recordHistories(IntegrationInfo info) {
+ if (StatusAge.stale(info)) {
+ return;
+ }
+ metrics.updateThroughputHistory(info);
+ metrics.updateEndpointHistory(info);
+ metrics.updateServiceHistory(info);
+ metrics.updateCbHistory(info);
+ metrics.updateHeapHistory(info);
+ if (ctx != null) {
+ ctx.healthHistory.record(info, System.currentTimeMillis() / 1000);
+ }
+ }
+
private List<Long> findPids(String name) {
return TuiHelper.findPids(name, statusFileResolver);
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
index bc5304001ff8..a559de4b30ce 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/IntegrationInfo.java
@@ -98,6 +98,13 @@ class IntegrationInfo {
int threadCount;
int peakThreadCount;
boolean vanishing;
+ // when it last wrote its status file (epoch millis), 0 when not known;
StatusAge tells one that stopped writing it
+ long statusWrittenAt;
+ // while it writes no status: the CPU time of its process, when it was
read (a MonotonicClock time), and the CPU
+ // since the read before in percent of one core (-1 not known yet)
+ long cpuMillis = -1;
+ long cpuReadAt;
+ double cpuPercent = -1;
// when it was found gone, a MonotonicClock time: it fades out from then
long vanishStart;
final List<RouteInfo> routes = new ArrayList<>();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
index 2486b66e6d78..76c4295defea 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/McpFacade.java
@@ -1726,7 +1726,8 @@ class McpFacade {
}
if ("stop-all".equals(action)) {
bridge.stopAll();
- return "Stopping all processes";
+ // the user answers the dialog: the processes stop when they
confirm it
+ return "Asked the user to stop all processes: they confirm it in
the Stop All dialog";
}
if (nameOrPid != null && !nameOrPid.isBlank()) {
String pid = findPidByNameOrPid(nameOrPid.trim());
@@ -1801,7 +1802,8 @@ class McpFacade {
}
case "stop" -> {
bridge.stopProcess(false);
- yield "Stopping " + name;
+ String stuck =
StatusAge.stopAnswer(ctx.findSelectedIntegration());
+ yield stuck != null ? stuck : "Stopping " + name;
}
case "kill" -> {
bridge.stopProcess(true);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index aaad494296c4..01f9c21a214d 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -526,7 +526,7 @@ class OverviewTab extends AbstractTab {
}
Line nameLine = Line.from(nameSpans);
Style failStyle = info.failed > 0 ? Theme.error().bold() :
Style.EMPTY;
- String throughputDisplay = info.throughput;
+ String throughputDisplay = StatusAge.stale(info) ? "-" :
info.throughput;
if (throughputDisplay == null ||
"0.00".equals(throughputDisplay)) {
LinkedList<Long> tpHist = throughputHistory.get(info.pid);
if (tpHist != null && !tpHist.isEmpty()) {
@@ -678,13 +678,12 @@ class OverviewTab extends AbstractTab {
} else if ("Running".equals(stateText) && info.routeStarted ==
0 && info.routeTotal > 0) {
stateText = "Stopped";
}
- if (info.reloadError != null && !"Stopping".equals(stateText))
{
- // its last reload failed, as camel ps shows it; the info
panel says why
- stateText = "Error";
- }
+ // its last reload failed, as camel ps shows it (the info
panel says why); or it wrote no status of late
+ stateText = StatusAge.state(info,
+ info.reloadError != null &&
!"Stopping".equals(stateText) ? "Error" : stateText);
Style statusStyle = switch (stateText) {
case "Started", "Running" -> Theme.success();
- case "Stopped", "Error" -> Theme.error();
+ case "Stopped", "Error", StatusAge.NO_STATUS ->
Theme.error();
default -> Theme.warning();
};
@@ -709,7 +708,7 @@ class OverviewTab extends AbstractTab {
nameSpans.add(Span.styled(" " + TuiIcons.README,
Style.EMPTY));
}
Line nameLine = TuiHelper.fitLine(nameSpans, nameColumn);
- String throughputDisplay = info.throughput;
+ String throughputDisplay = StatusAge.stale(info) ? "-" :
info.throughput;
if (throughputDisplay == null ||
"0.00".equals(throughputDisplay)) {
LinkedList<Long> tpHist = throughputHistory.get(info.pid);
if (tpHist != null && !tpHist.isEmpty()) {
@@ -1633,6 +1632,7 @@ class OverviewTab extends AbstractTab {
if (info.reloadError != null) {
row.put("reloadError", info.reloadError);
}
+ StatusAge.describe(info, row);
row.put("ready", info.ready);
// info.uptime holds the process start time (epoch millis), which
the screen shows as an elapsed
// duration; export the same duration plus the raw values with
unambiguous names so an AI reading
@@ -1645,7 +1645,8 @@ class OverviewTab extends AbstractTab {
row.put("exchangesTotal", info.exchangesTotal);
row.put("failed", info.failed);
row.put("inflight", info.inflight);
- row.put("throughput", info.throughput);
+ // its rate now is not known without a status of late
+ row.put("throughput", StatusAge.stale(info) ? null :
info.throughput);
row.put("routeStarted", info.routeStarted);
row.put("routeTotal", info.routeTotal);
row.put("meanTime", info.meanTime);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusAge.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusAge.java
new file mode 100644
index 000000000000..df9888bbc817
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusAge.java
@@ -0,0 +1,141 @@
+/*
+ * 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.time.Duration;
+import java.time.Instant;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import dev.tamboui.text.Span;
+import org.apache.camel.util.TimeUtils;
+import org.apache.camel.util.json.JsonObject;
+
+/**
+ * An integration that wrote no status of late. It writes its status file
every second; when it has not for a while, and
+ * its process is still there, the numbers the TUI has are those of that
moment: the JVM is stuck (a heap that is full,
+ * where the garbage collector runs all the time; a debugger that suspended
it), or its CLI connector stopped
+ * (CAMEL-26794). The screens then say so instead of showing it running at its
last rate.
+ */
+final class StatusAge {
+
+ /** The state of an integration without a status of late, in the STATUS
column. */
+ static final String NO_STATUS = "No status";
+
+ // the status is written every second (100 ms while debugging), and a
pause of the garbage collector takes seconds
+ static final long AFTER_MILLIS = 15_000;
+ // the CPU of a JVM that is busy without a status: the garbage collector,
or a loop
+ private static final double BUSY_CPU_PERCENT = 50;
+ private static final DateTimeFormatter CLOCK =
DateTimeFormatter.ofPattern("HH:mm:ss", Locale.US);
+
+ private StatusAge() {
+ }
+
+ /** Whether the integration wrote no status for a while, at the time now
(epoch millis). */
+ static boolean stale(IntegrationInfo info, long now) {
+ return !info.phantom && !info.vanishing && info.statusWrittenAt > 0 &&
now - info.statusWrittenAt > AFTER_MILLIS;
+ }
+
+ static boolean stale(IntegrationInfo info) {
+ return stale(info, System.currentTimeMillis());
+ }
+
+ /** The state to show: {@link #NO_STATUS} when it wrote no status of late,
else the state it has. */
+ static String state(IntegrationInfo info, String state) {
+ return stale(info) ? NO_STATUS : state;
+ }
+
+ /**
+ * The CPU the process used since the last read, in percent of one core,
while it writes no status: the JVM that is
+ * busy (the garbage collector) is told from the one that is held (a
debugger, a deadlock).
+ *
+ * @param now a MonotonicClock time
+ */
+ static void observeCpu(IntegrationInfo info, IntegrationInfo previous,
ProcessHandle process, long now) {
+ if (!stale(info)) {
+ return;
+ }
+ info.cpuMillis =
process.info().totalCpuDuration().map(Duration::toMillis).orElse(-1L);
+ info.cpuReadAt = now;
+ if (previous == null) {
+ return;
+ }
+ if (info.cpuMillis >= 0 && previous.cpuMillis >= 0 && now >
previous.cpuReadAt) {
+ info.cpuPercent = 100.0 * (info.cpuMillis - previous.cpuMillis) /
(now - previous.cpuReadAt);
+ } else {
+ info.cpuPercent = previous.cpuPercent;
+ }
+ }
+
+ /** The Watch line of About for an integration that wrote no status of
late, else none. */
+ static List<Span> watch(IntegrationInfo info) {
+ List<Span> spans = new ArrayList<>();
+ if (!stale(info)) {
+ return spans;
+ }
+ spans.add(Span.styled("✗ no status since " +
clock(info.statusWrittenAt), Theme.error()));
+ spans.add(Span.styled(" (" +
TimeUtils.printSince(info.statusWrittenAt) + " ago)", Theme.muted()));
+ String why = why(info.cpuPercent);
+ if (why != null) {
+ spans.add(Span.styled(": " + why, Theme.error()));
+ }
+ spans.add(Span.styled(" · its numbers are of then, and a stop may not
reach it", Theme.muted()));
+ spans.add(Span.raw(" "));
+ spans.add(Span.styled(" F10 ", Theme.hintKey()));
+ spans.add(Span.raw(" stop or kill"));
+ return spans;
+ }
+
+ /** What the CPU of a JVM without a status says, or null when it is not
known yet. */
+ static String why(double cpuPercent) {
+ if (cpuPercent < 0) {
+ return null;
+ }
+ String cpu = String.format(Locale.US, "%.0f%% CPU", cpuPercent);
+ return cpuPercent >= BUSY_CPU_PERCENT
+ ? "busy at " + cpu + ", as when its heap is full and the
garbage collector runs all the time"
+ : "idle at " + cpu + ", as when a debugger holds it";
+ }
+
+ /** For the row of the integration that a tool reads: since when it wrote
no status, and its CPU. */
+ static void describe(IntegrationInfo info, JsonObject row) {
+ if (!stale(info)) {
+ return;
+ }
+ row.put("noStatusSince",
Instant.ofEpochMilli(info.statusWrittenAt).toString());
+ if (info.cpuPercent >= 0) {
+ row.put("cpuPercent", Math.round(info.cpuPercent));
+ }
+ }
+
+ /** The answer of a stop of an integration that wrote no status of late,
else null. */
+ static String stopAnswer(IntegrationInfo info) {
+ if (info == null || !stale(info)) {
+ return null;
+ }
+ return "Stopping " + info.name + ", but it wrote no status for " +
TimeUtils.printSince(info.statusWrittenAt)
+ + ", so it may not read the stop either: kill stops it";
+ }
+
+ private static String clock(long epochMillis) {
+ return LocalTime.ofInstant(Instant.ofEpochMilli(epochMillis),
ZoneId.systemDefault()).format(CLOCK);
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
index a7e9943d64dd..5ed841d8d90c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/overview.md
@@ -35,7 +35,11 @@ happened to it, with no AI:
when the last one did, routes not started, exchanges in flight. Errors
that stopped more than a minute ago are history: the line says there
were none in the last minute, with the earlier ones dimmed (Last 5m and
- What happened still show them).
+ What happened still show them). An integration that wrote no status
+ for 15 seconds says since when instead, and what its CPU says: busy
+ when its heap is full and the garbage collector runs all the time,
+ idle when a debugger holds it. Its numbers are of then, and a stop
+ may not reach it; `F10` kills it.
- **Next** — the keys that lead deeper: **4** the Timeline, **5** the
Diagram, **2** the source.
- **What happened** — a strip per route over the last 5 minutes (as wide
@@ -70,7 +74,7 @@ Each row represents one running Camel integration:
- **PID** — Process ID of the JVM running this integration
- **NAME** — Name of the integration (from the route file or application
configuration). Example: `camel-demo`, `my-app`
- **VERSION** — Camel version the integration is running on (e.g., `4.21.0`)
-- **STATUS** — Current lifecycle state: `Running` (processing messages),
`Started` (ready), or `Stopped`
+- **STATUS** — Current lifecycle state: `Running` (processing messages),
`Started` (ready), or `Stopped`; `No status` when it wrote no status for 15
seconds while its process runs (its MSG/S is then `-`)
- **AGE** — How long the integration has been running (e.g., `2m30s`, `1h15m`)
- **MSG/S** or **MSG/M** — Messages processed per second or per minute
(current throughput). The rate unit can be configured in settings. This is the
overall rate across all routes
- **TOTAL** — Total number of exchanges (messages) processed since the
integration started
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
index 245204ed0ee2..0bdb12d6c214 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
@@ -156,6 +156,25 @@ class AboutViewTest {
assertThat(render(new AboutView(ctx), info, 200, 40)).contains("last
5m, a column is 2s");
}
+ @Test
+ void anIntegrationWithoutAStatusOfLateSaysSoInsteadOfRunning() {
+ IntegrationInfo info = new IntegrationInfo();
+ info.pid = "1234";
+ info.errorCount = 2;
+ MonitorContext ctx = new MonitorContext(new
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+ info.statusWrittenAt = System.currentTimeMillis();
+ assertThat(render(new AboutView(ctx), info)).contains("●
running").contains("✗ 2 errors");
+
+ // a timer-log held with kill -STOP: its last status 30 seconds ago,
and no CPU since
+ info.statusWrittenAt = System.currentTimeMillis() - 30_000;
+ info.cpuPercent = 0;
+ String rendered = render(new AboutView(ctx), info);
+ assertThat(rendered).contains("✗ no status").doesNotContain("●
running")
+ .contains("(30s ago): idle at 0% CPU, as when a debugger holds
it")
+ // what is to watch now; its errors are of then
+ .doesNotContain("2 errors");
+ }
+
@Test
void watchCalmsDownWhenTheErrorsStopped() {
IntegrationInfo info = new IntegrationInfo();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshMembershipTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshMembershipTest.java
index 7f59653f8e34..ac61da9bcc27 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshMembershipTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/DataRefreshMembershipTest.java
@@ -31,7 +31,8 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* A new or stopped integration is noticed on every tab: the cheap look at the
status files in ~/.camel asks for a full
- * scan when they change, at most once a second. Lookups that wait for the
diagram retry until found.
+ * scan when they change, at most once a second. Lookups that wait for the
diagram retry until found. A status that is
+ * not of late adds nothing to the charts.
*/
@Isolated
class DataRefreshMembershipTest {
@@ -109,6 +110,23 @@ class DataRefreshMembershipTest {
assertThat(calls.get()).isEqualTo(1);
}
+ @Test
+ void aStatusOfThenAddsNothingToTheCharts() {
+ DataRefreshService service = newService();
+ IntegrationInfo info = new IntegrationInfo();
+ info.pid = "562838";
+ info.throughput = "1.00";
+ info.statusWrittenAt = System.currentTimeMillis();
+ service.recordHistories(info);
+
assertThat(service.metrics().getThroughputHistory().get("562838")).hasSize(1);
+
+ // held with kill -STOP a minute ago: its status still says 1 msg/s,
and the chart does not take it on
+ info.statusWrittenAt = System.currentTimeMillis() - 60_000;
+ service.metrics().getThroughputHistory().clear();
+ service.recordHistories(info);
+
assertThat(service.metrics().getThroughputHistory()).doesNotContainKey("562838");
+ }
+
@Test
void anExampleIsSelectedByTheNameItGivesItself() {
assertThat(DataRefreshService.isLaunched("fail-well/circuit-breaker",
"circuit-breaker")).isTrue();
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusAgeTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusAgeTest.java
new file mode 100644
index 000000000000..db6145dcca8a
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/StatusAgeTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.time.Instant;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.text.Span;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The memory-leak example after a few hours: its heap is full, the garbage
collector runs all the time, and it wrote
+ * its status file the last time at 18:36. The TUI showed it running at 8.52
msg/s since; now it says it has no status.
+ */
+class StatusAgeTest {
+
+ private IntegrationInfo info;
+ private MonitorContext ctx;
+
+ @BeforeEach
+ void setUp() {
+ Theme.resetForTesting();
+ info = new IntegrationInfo();
+ info.pid = "500874";
+ info.name = "memory-leak";
+ info.state = 5;
+ info.throughput = "8.52";
+ info.statusWrittenAt = System.currentTimeMillis();
+ ctx = new MonitorContext(new AtomicReference<>(List.of(info)), new
AtomicReference<>(List.of()));
+ ctx.selectedPid = info.pid;
+ }
+
+ @AfterEach
+ void tearDown() {
+ Theme.resetForTesting();
+ }
+
+ @Test
+ void aStatusOfAFewSecondsAgoIsNoReason() {
+ long now = System.currentTimeMillis();
+ info.statusWrittenAt = now - 5_000;
+ assertThat(StatusAge.stale(info, now)).isFalse();
+ info.statusWrittenAt = now - StatusAge.AFTER_MILLIS - 1;
+ assertThat(StatusAge.stale(info, now)).isTrue();
+ // not known, or an opened project that does not run
+ info.statusWrittenAt = 0;
+ assertThat(StatusAge.stale(info, now)).isFalse();
+ info.statusWrittenAt = now - 60_000;
+ info.phantom = true;
+ assertThat(StatusAge.stale(info, now)).isFalse();
+ }
+
+ @Test
+ void theOverviewSaysItHasNoStatusInsteadOfItsLastRate() {
+ stuckFor(4 * 60_000);
+
+ OverviewTab tab = new OverviewTab(ctx, new MetricsCollector(), new
HashSet<>(), () -> {
+ }).showIntegrations();
+ String row = TuiTestHelper.renderToString(tab, 160, 30).lines()
+ .filter(l ->
l.contains("memory-leak")).findFirst().orElseThrow();
+
assertThat(row).contains(StatusAge.NO_STATUS).doesNotContain("Running").doesNotContain("8.52");
+
+ JsonObject json = (JsonObject) ((JsonArray)
tab.getTableDataAsJson().get("rows")).get(0);
+ assertThat(json).containsEntry("noStatusSince",
Instant.ofEpochMilli(info.statusWrittenAt).toString())
+ .containsEntry("cpuPercent", 642L).containsEntry("throughput",
null);
+ }
+
+ @Test
+ void theWatchLineSaysSinceWhenAndWhatItsCpuSays() {
+ stuckFor(4 * 60_000);
+
+ String watch = text(StatusAge.watch(info));
+ assertThat(watch).startsWith("✗ no status since ").contains("(4m")
+ .contains("busy at 642% CPU, as when its heap is full")
+ .contains("F10 stop or kill");
+
+ info.cpuPercent = 0.4;
+ assertThat(text(StatusAge.watch(info))).contains("idle at 0% CPU, as
when a debugger holds it");
+ // its CPU is not known before the second read
+ info.cpuPercent = -1;
+ assertThat(text(StatusAge.watch(info))).doesNotContain("CPU");
+ // and a running integration has no such line
+ info.statusWrittenAt = System.currentTimeMillis();
+ assertThat(StatusAge.watch(info)).isEmpty();
+ }
+
+ @Test
+ void aStopSaysItMayNotReachIt() {
+ assertThat(StatusAge.stopAnswer(info)).isNull();
+ stuckFor(4 * 60_000);
+ assertThat(StatusAge.stopAnswer(info))
+ .isEqualTo("Stopping memory-leak, but it wrote no status for
4m0s, so it may not read the stop either:"
+ + " kill stops it");
+ }
+
+ @Test
+ void theCpuIsMeasuredBetweenTwoReadsWhileItHasNoStatus() {
+ ProcessHandle self = ProcessHandle.current();
+ StatusAge.observeCpu(info, null, self, 1_000);
+ // it wrote its status: nothing to measure
+ assertThat(info.cpuMillis).isEqualTo(-1);
+
+ info.statusWrittenAt = System.currentTimeMillis() - 60_000;
+ IntegrationInfo previous = new IntegrationInfo();
+ previous.cpuMillis = 0;
+ previous.cpuReadAt = 1_000;
+ StatusAge.observeCpu(info, previous, self, 2_000);
+ assertThat(info.cpuReadAt).isEqualTo(2_000);
+ // the CPU of this JVM since it started, over one second
+ assertThat(info.cpuPercent).isGreaterThan(0);
+ }
+
+ private void stuckFor(long millis) {
+ info.statusWrittenAt = System.currentTimeMillis() - millis;
+ info.cpuPercent = 642;
+ }
+
+ private static String text(List<Span> spans) {
+ StringBuilder sb = new StringBuilder();
+ spans.forEach(s -> sb.append(s.content()));
+ return sb.toString();
+ }
+}