Copilot commented on code in PR #7674:
URL: https://github.com/apache/shenyu/pull/7674#discussion_r4291647693
##########
shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/cache/UpstreamCheckTask.java:
##########
@@ -156,6 +185,11 @@ public void run() {
}
private void healthCheck() {
+ final long startNanos = System.nanoTime();
+ currentRoundSubmittedChecks.set(0);
+ currentRoundMaxEndpointLatencyMillis.set(0L);
Review Comment:
A concurrent run() resets these shared counters before acquiring lock, so it
can erase the active round's submitted count or maximum latency. Recording
metrics after releasing lock also lets the next invocation reset the maximum
before it is captured. Isolate counters per round and pass that context to its
probes, or serialize reset, capture and publication while accounting for probes
still running after an exception.
##########
shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/cache/UpstreamCheckTask.java:
##########
@@ -164,14 +198,70 @@ private void healthCheck() {
synchronized (lock) {
if (tryStartHealthCheck()) {
doHealthCheck();
+ submitted = currentRoundSubmittedChecks.get();
+ pendingAtWait = countPendingFutures();
waitFinish();
}
}
} catch (Exception e) {
LOG.error("[Health Check] Meet problem: ", e);
} finally {
finishHealthCheck();
+ recordRoundMetrics(startNanos, submitted, pendingAtWait);
+ }
+ }
+
+ private int countPendingFutures() {
+ int pending = 0;
+ for (CompletableFuture<UpstreamWithSelectorId> future : futures) {
+ if (!future.isDone()) {
+ pending++;
+ }
}
+ return pending;
+ }
+
+ private void recordRoundMetrics(final long startNanos, final int
submitted, final int pendingAtWait) {
+ final long durationMillis =
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
+ final long maxEndpointLatencyMillis =
currentRoundMaxEndpointLatencyMillis.get();
+ final long oldestHealthStateMillis = computeOldestHealthStateMillis();
+ final long completedAtMillis = System.currentTimeMillis();
+ this.lastRoundDurationMillis = durationMillis;
+ this.lastRoundSubmittedChecks = submitted;
+ this.lastRoundPendingChecks = pendingAtWait;
+ this.lastRoundMaxEndpointLatencyMillis = maxEndpointLatencyMillis;
+ this.lastRoundOldestHealthStateMillis = oldestHealthStateMillis;
Review Comment:
Separate volatile writes do not publish the coherent snapshot promised above
the scratch counters. A monitoring thread can read the new duration while
submitted/pending counts, latency or completion time still belong to the
previous round. Publish an immutable round-metrics object through one volatile
reference and expose a snapshot accessor so consumers can read one consistent
round; separate scalar getters alone cannot guarantee that.
##########
shenyu-loadbalancer/src/main/java/org/apache/shenyu/loadbalancer/cache/UpstreamCheckTask.java:
##########
@@ -164,14 +198,70 @@ private void healthCheck() {
synchronized (lock) {
if (tryStartHealthCheck()) {
doHealthCheck();
+ submitted = currentRoundSubmittedChecks.get();
+ pendingAtWait = countPendingFutures();
waitFinish();
}
}
} catch (Exception e) {
LOG.error("[Health Check] Meet problem: ", e);
} finally {
finishHealthCheck();
+ recordRoundMetrics(startNanos, submitted, pendingAtWait);
+ }
+ }
+
+ private int countPendingFutures() {
+ int pending = 0;
+ for (CompletableFuture<UpstreamWithSelectorId> future : futures) {
+ if (!future.isDone()) {
+ pending++;
+ }
}
+ return pending;
+ }
+
+ private void recordRoundMetrics(final long startNanos, final int
submitted, final int pendingAtWait) {
+ final long durationMillis =
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNanos);
+ final long maxEndpointLatencyMillis =
currentRoundMaxEndpointLatencyMillis.get();
+ final long oldestHealthStateMillis = computeOldestHealthStateMillis();
+ final long completedAtMillis = System.currentTimeMillis();
+ this.lastRoundDurationMillis = durationMillis;
+ this.lastRoundSubmittedChecks = submitted;
+ this.lastRoundPendingChecks = pendingAtWait;
+ this.lastRoundMaxEndpointLatencyMillis = maxEndpointLatencyMillis;
+ this.lastRoundOldestHealthStateMillis = oldestHealthStateMillis;
+ this.lastRoundCompletedAtMillis = completedAtMillis;
+ totalRounds.incrementAndGet();
+ totalSubmittedChecks.addAndGet(submitted);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("[Health Check] round summary: durationMs={},
submitted={}, pendingAtWait={}, "
+ + "maxEndpointLatencyMs={},
oldestHealthStateMs={}, healthySelectors={}, unhealthySelectors={}",
+ durationMillis, submitted, pendingAtWait,
maxEndpointLatencyMillis, oldestHealthStateMillis,
+ healthyUpstream.size(), unhealthyUpstream.size());
+ }
+ }
+
+ private long computeOldestHealthStateMillis() {
+ final long now = System.currentTimeMillis();
+ long oldest = 0L;
+ for (List<Upstream> bucket : healthyUpstream.values()) {
+ for (Upstream upstream : bucket) {
+ final long age = now - upstream.getLastHealthTimestamp();
+ if (age > oldest) {
+ oldest = age;
+ }
+ }
+ }
+ for (List<Upstream> bucket : unhealthyUpstream.values()) {
+ for (Upstream upstream : bucket) {
+ final long age = now - upstream.getLastUnhealthyTimestamp();
+ if (age > oldest) {
+ oldest = age;
+ }
+ }
+ }
Review Comment:
Health-check-disabled upstreams can permanently dominate this metric with an
age since 1970. UpstreamCacheManager.initializeUpstreamHealthStatus() marks
them healthy without setting lastHealthTimestamp, and check() leaves that zero
timestamp unchanged while they remain healthy. Exclude disabled entries and
unset timestamps in both buckets so they do not hide the ages of actual health
observations.
##########
shenyu-loadbalancer/src/test/java/org/apache/shenyu/loadbalancer/cache/UpstreamCheckTaskTest.java:
##########
@@ -303,4 +303,125 @@ public void testMoveUpstreamBetweenMaps() {
// Clean up
healthCheckTask.triggerRemoveAll(selectorId);
}
+
+ /**
+ * The observability counters added for #7647 must be populated after a
synchronous round
+ * completes, so operators can compare round duration, submitted probes
and pending probes
+ * against the configured checkInterval without adding a profiler to the
runtime.
+ */
+ @Test
+ @Timeout(30000)
+ public void roundMetricsAreRecordedAfterRunCompletes() {
+ final UpstreamCheckTask task = new UpstreamCheckTask(50000);
+ task.setPoolSize(2);
+ task.setCheckTimeout(500);
+ task.schedule();
+
+ final String selectorId = "metrics-round";
+ for (int i = 0; i < 4; i++) {
+ final Upstream upstream = Upstream.builder()
+ .url("127.0.0.1:" + (65000 + i))
+ .build();
+ upstream.setHealthy(true);
+ upstream.setLastHealthTimestamp(System.currentTimeMillis());
+ task.triggerAddOne(selectorId, upstream);
+ }
+
+ final long before = task.getTotalRounds();
+ task.run();
+
+ assertTrue(task.getTotalRounds() > before, "totalRounds must advance
after run()");
+ assertTrue(task.getLastRoundDurationMillis() >= 0L, "duration must be
non-negative");
+ assertTrue(task.getLastRoundSubmittedChecks() >= 4,
+ "submitted probes must cover every registered upstream, got "
+ task.getLastRoundSubmittedChecks());
+ assertTrue(task.getLastRoundPendingChecks() >= 0, "pending probes must
be non-negative");
+ assertTrue(task.getLastRoundCompletedAtMillis() > 0L, "completion
timestamp must be set");
+ assertTrue(task.getTotalSubmittedChecks() >=
task.getLastRoundSubmittedChecks(),
+ "cumulative submitted must include the last round");
+ assertTrue(task.getLastRoundOldestHealthStateMillis() >= 0L,
+ "oldest health-state age must be non-negative");
+
+ task.triggerRemoveAll(selectorId);
+ }
+
+ /**
+ * An empty population still completes a round and updates the round
counter, so operators
+ * can distinguish "task not running" from "task running with nothing to
check".
+ */
+ @Test
+ @Timeout(10000)
+ public void emptyPopulationStillRecordsRoundCompletion() {
+ final UpstreamCheckTask task = new UpstreamCheckTask(50000);
+ task.setPoolSize(1);
+ task.schedule();
+
+ final long before = task.getTotalRounds();
+ task.run();
+
+ assertTrue(task.getTotalRounds() > before, "round counter must advance
even with no upstreams");
+ assertTrue(task.getLastRoundDurationMillis() >= 0L, "duration must be
non-negative");
+ assertTrue(task.getLastRoundSubmittedChecks() == 0, "no probes should
be submitted for an empty population");
+ assertTrue(task.getLastRoundPendingChecks() == 0, "no probes should be
pending for an empty population");
+ assertTrue(task.getLastRoundMaxEndpointLatencyMillis() == 0L,
+ "endpoint latency must stay zero when no probe ran");
+ }
+
+ /**
+ * Upstreams registered with a stale {@code lastHealthTimestamp} must
surface through
+ * {@code lastRoundOldestHealthStateMillis}, so the metric actually
reflects health-state age
+ * instead of collapsing to zero.
+ */
+ @Test
+ @Timeout(10000)
+ public void oldestHealthStateAgeReflectsStaleTimestamp() {
+ final UpstreamCheckTask task = new UpstreamCheckTask(50000);
+ task.setPoolSize(1);
+ task.schedule();
+
+ final String selectorId = "metrics-stale";
+ final Upstream stale =
Upstream.builder().url("127.0.0.1:65100").build();
Review Comment:
This test assumes port 65100 has no listener. If another process is
listening, checkUrl() succeeds and refreshes lastHealthTimestamp, causing the
stale-age assertion to fail. A blank URL returns false without network I/O and
preserves the stale timestamp under this test's configured threshold.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]