This is an automated email from the ASF dual-hosted git repository.

JackieTien97 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new 49336d47731 [Metrics] Fix concurrent Prometheus scrapes and disk 
sampling (#18957)
49336d47731 is described below

commit 49336d477313b504182ff9627b3ebcf28d1f623a
Author: Jackie Tien <[email protected]>
AuthorDate: Wed Oct 21 19:32:06 2026 +0800

    [Metrics] Fix concurrent Prometheus scrapes and disk sampling (#18957)
---
 .../iotdb/db/it/metric/IoTDBMetricFormatIT.java    | 109 +++++++++++++++
 .../apache/iotdb/db/it/metric/IoTDBMetricIT.java   |   5 +-
 .../metricsets/disk/LinuxDiskMetricsManager.java   | 120 +++++++++++------
 .../reporter/prometheus/PrometheusReporter.java    |  33 +++--
 .../disk/LinuxDiskMetricsManagerTest.java          | 149 +++++++++++++++++++++
 .../prometheus/PrometheusReporterTest.java         | 131 ++++++++++++++++++
 6 files changed, 492 insertions(+), 55 deletions(-)

diff --git 
a/integration-test/src/test/java/org/apache/iotdb/db/it/metric/IoTDBMetricFormatIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/db/it/metric/IoTDBMetricFormatIT.java
new file mode 100644
index 00000000000..a4cf8a59208
--- /dev/null
+++ 
b/integration-test/src/test/java/org/apache/iotdb/db/it/metric/IoTDBMetricFormatIT.java
@@ -0,0 +1,109 @@
+/*
+ * 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.iotdb.db.it.metric;
+
+import org.apache.iotdb.it.framework.IoTDBTestRunner;
+import org.apache.iotdb.itbase.category.ClusterIT;
+import org.apache.iotdb.itbase.category.LocalStandaloneIT;
+import org.apache.iotdb.metrics.impl.DoNothingAutoGauge;
+import org.apache.iotdb.metrics.impl.DoNothingMetricManager;
+import org.apache.iotdb.metrics.reporter.prometheus.PrometheusReporter;
+import org.apache.iotdb.metrics.type.IMetric;
+import org.apache.iotdb.metrics.utils.MetricInfo;
+import org.apache.iotdb.metrics.utils.MetricType;
+
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+@RunWith(IoTDBTestRunner.class)
+@Category({LocalStandaloneIT.class, ClusterIT.class})
+public class IoTDBMetricFormatIT {
+  private static final String SAMPLE =
+      
"disk_io_busy_percentage{cluster=\"defaultCluster\",nodeType=\"CONFIGNODE\",nodeId=\"0\",disk_id=\"vda\",}
 ";
+
+  @Test
+  public void testValidSampleValues() {
+    for (String value :
+        new String[] {
+          "NaN",
+          "nan",
+          "Inf",
+          "+Inf",
+          "-Inf",
+          "Infinity",
+          "+Infinity",
+          "-Infinity",
+          "iNfInItY",
+          "+inf",
+          "-infinity",
+          "0",
+          "-0.25",
+          "1.0e-3"
+        }) {
+      assertTrue(value, IoTDBMetricIT.isValidPrometheusTextFormat(SAMPLE + 
value));
+    }
+  }
+
+  @Test
+  public void testInvalidSampleValues() {
+    for (String value :
+        new String[] {
+          "", "Infinite", "Infinitygarbage", "--Inf", "+NaN", "-NaN", "null", 
"1.0garbage"
+        }) {
+      assertFalse(value, IoTDBMetricIT.isValidPrometheusTextFormat(SAMPLE + 
value));
+    }
+  }
+
+  @Test
+  public void testReporterSpecialValuesRoundTrip() throws Exception {
+    Method scrape = PrometheusReporter.class.getDeclaredMethod("scrape");
+    scrape.setAccessible(true);
+    for (double sampleValue :
+        new double[] {Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, 
Double.NaN}) {
+      DoNothingAutoGauge sampleGauge =
+          new DoNothingAutoGauge() {
+            @Override
+            public double getValue() {
+              return sampleValue;
+            }
+          };
+      PrometheusReporter reporter =
+          new PrometheusReporter(
+              new DoNothingMetricManager() {
+                @Override
+                public Map<MetricInfo, IMetric> getAllMetrics() {
+                  return Collections.singletonMap(
+                      new MetricInfo(MetricType.AUTO_GAUGE, "special_value"), 
sampleGauge);
+                }
+              });
+      String metrics = (String) scrape.invoke(reporter);
+      assertTrue(metrics, metrics.contains("} " + Double.toString(sampleValue) 
+ "\n"));
+      assertTrue(metrics, IoTDBMetricIT.isValidPrometheusTextFormat(metrics));
+    }
+  }
+}
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/db/it/metric/IoTDBMetricIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/db/it/metric/IoTDBMetricIT.java
index 3e6f660d4f2..1d1b06b11ad 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/db/it/metric/IoTDBMetricIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/db/it/metric/IoTDBMetricIT.java
@@ -49,6 +49,7 @@ public class IoTDBMetricIT {
   private static final String METRIC_NAME_REGEX = "[a-zA-Z_:][a-zA-Z0-9_:]*";
   private static final String LABEL_NAME_REGEX = "[a-zA-Z_][a-zA-Z0-9_]*";
   private static final String LABEL_VALUE_REGEX = "[^\",]+";
+  // Go ParseFloat accepts case-insensitive NaN, Inf and Infinity, with signs 
only for infinities.
   private static final String METRIC_LINE_REGEX =
       METRIC_NAME_REGEX
           + "(\\{"
@@ -59,7 +60,7 @@ public class IoTDBMetricIT {
           + LABEL_NAME_REGEX
           + "=\""
           + LABEL_VALUE_REGEX
-          + "\",)*})? [+-]?[0-9]*\\.?[0-9]+([eE][+-]?[0-9]+)?";
+          + "\",)*})? 
([+-]?[0-9]*\\.?[0-9]+([eE][+-]?[0-9]+)?|(?i:NaN|[+-]?Inf(?:inity)?))";
   private static final String HELP_PREFIX = "# HELP ";
   private static final String HELP_REGEX = HELP_PREFIX + METRIC_NAME_REGEX;
   private static final String TYPE_PREFIX = "# TYPE ";
@@ -74,7 +75,7 @@ public class IoTDBMetricIT {
   private static final String WRONG_USERNAME = "bad";
   private static final String WRONG_PASSWORD = "!!";
 
-  private static boolean isValidPrometheusTextFormat(String metrics) {
+  static boolean isValidPrometheusTextFormat(String metrics) {
     String[] lines = metrics.split("\\n");
     boolean valid = true;
 
diff --git 
a/iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/metricsets/disk/LinuxDiskMetricsManager.java
 
b/iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/metricsets/disk/LinuxDiskMetricsManager.java
index cb8ea23a821..b9c65f69091 100644
--- 
a/iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/metricsets/disk/LinuxDiskMetricsManager.java
+++ 
b/iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/metricsets/disk/LinuxDiskMetricsManager.java
@@ -28,13 +28,17 @@ import org.slf4j.LoggerFactory;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Scanner;
 import java.util.Set;
+import java.util.function.LongSupplier;
 import java.util.stream.Collectors;
 
 /**
@@ -59,10 +63,10 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   @SuppressWarnings("squid:S1075")
   private static final String DISK_ID_PATH = "/sys/block";
 
-  @SuppressWarnings("squid:S1075")
-  private static final String DISK_SECTOR_SIZE_PATH = 
"/sys/block/%s/queue/hw_sector_size";
-
-  private final String processIoStatusPath;
+  private final Path diskStatsPath;
+  private final Path diskIdPath;
+  private final Path processIoStatusPath;
+  private final LongSupplier clock;
   private static final int DISK_ID_OFFSET = 3;
   private static final int DISK_READ_COUNT_OFFSET = 4;
   private static final int DISK_MERGED_READ_COUNT_OFFSET = 5;
@@ -77,7 +81,7 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   private static final int DEFAULT_SECTOR_SIZE = 512;
   private static final double BYTES_PER_KB = 1024.0;
   private static final long UPDATE_SMALLEST_INTERVAL = 10000L;
-  private Set<String> diskIdSet;
+  private Set<String> diskIdSet = Collections.emptySet();
   private final Map<String, Integer> diskSectorSizeMap;
   private long lastUpdateTime = 0L;
   private long updateInterval = 1L;
@@ -113,9 +117,21 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   private long lastWriteOpsCountForProcess = 0L;
 
   public LinuxDiskMetricsManager() {
-    processIoStatusPath =
-        String.format(
-            "/proc/%s/io", 
MetricConfigDescriptor.getInstance().getMetricConfig().getPid());
+    this(
+        Paths.get(DISK_STATUS_FILE_PATH),
+        Paths.get(DISK_ID_PATH),
+        Paths.get(
+            String.format(
+                "/proc/%s/io", 
MetricConfigDescriptor.getInstance().getMetricConfig().getPid())),
+        System::currentTimeMillis);
+  }
+
+  LinuxDiskMetricsManager(
+      Path diskStatsPath, Path diskIdPath, Path processIoStatusPath, 
LongSupplier clock) {
+    this.diskStatsPath = diskStatsPath;
+    this.diskIdPath = diskIdPath;
+    this.processIoStatusPath = processIoStatusPath;
+    this.clock = clock;
     collectDiskId();
     // leave one entry to avoid hashmap resizing
     diskSectorSizeMap = new HashMap<>(diskIdSet.size() + 1, 1);
@@ -143,7 +159,7 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   @Override
-  public Map<String, Double> getReadDataSizeForDisk() {
+  public synchronized Map<String, Double> getReadDataSizeForDisk() {
     checkUpdate();
     Map<String, Double> readDataMap = new HashMap<>(diskIdSet.size());
     for (Map.Entry<String, Long> entry : 
lastReadSectorCountForDisk.entrySet()) {
@@ -154,7 +170,7 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   @Override
-  public Map<String, Double> getWriteDataSizeForDisk() {
+  public synchronized Map<String, Double> getWriteDataSizeForDisk() {
     checkUpdate();
     Map<String, Double> writeDataMap = new HashMap<>(diskIdSet.size());
     for (Map.Entry<String, Long> entry : 
lastWriteSectorCountForDisk.entrySet()) {
@@ -165,28 +181,32 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   @Override
-  public Map<String, Long> getReadOperationCountForDisk() {
+  public synchronized Map<String, Long> getReadOperationCountForDisk() {
     checkUpdate();
-    return lastReadOperationCountForDisk;
+    return new HashMap<>(lastReadOperationCountForDisk);
   }
 
   @Override
-  public Map<String, Long> getWriteOperationCountForDisk() {
-    return lastWriteOperationCountForDisk;
+  public synchronized Map<String, Long> getWriteOperationCountForDisk() {
+    checkUpdate();
+    return new HashMap<>(lastWriteOperationCountForDisk);
   }
 
   @Override
-  public Map<String, Long> getReadCostTimeForDisk() {
-    return lastReadTimeCostForDisk;
+  public synchronized Map<String, Long> getReadCostTimeForDisk() {
+    checkUpdate();
+    return new HashMap<>(lastReadTimeCostForDisk);
   }
 
   @Override
-  public Map<String, Long> getWriteCostTimeForDisk() {
-    return lastWriteTimeCostForDisk;
+  public synchronized Map<String, Long> getWriteCostTimeForDisk() {
+    checkUpdate();
+    return new HashMap<>(lastWriteTimeCostForDisk);
   }
 
   @Override
-  public Map<String, Double> getIoUtilsPercentage() {
+  public synchronized Map<String, Double> getIoUtilsPercentage() {
+    checkUpdate();
     Map<String, Double> utilsMap = new HashMap<>(diskIdSet.size());
     for (Map.Entry<String, Long> entry : 
incrementIoBusyTimeForDisk.entrySet()) {
       utilsMap.put(entry.getKey(), ((double) entry.getValue()) / 
updateInterval);
@@ -195,7 +215,8 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   @Override
-  public Map<String, Double> getAvgReadCostTimeOfEachOpsForDisk() {
+  public synchronized Map<String, Double> getAvgReadCostTimeOfEachOpsForDisk() 
{
+    checkUpdate();
     Map<String, Double> avgReadTimeCostMap = new HashMap<>(diskIdSet.size());
     for (Map.Entry<String, Long> readCostEntry : 
incrementReadTimeCostForDisk.entrySet()) {
       // use Long.max to avoid NaN
@@ -212,7 +233,8 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   @Override
-  public Map<String, Double> getAvgWriteCostTimeOfEachOpsForDisk() {
+  public synchronized Map<String, Double> 
getAvgWriteCostTimeOfEachOpsForDisk() {
+    checkUpdate();
     Map<String, Double> avgWriteTimeCostMap = new HashMap<>(diskIdSet.size());
     for (Map.Entry<String, Long> writeCostEntry : 
incrementWriteTimeCostForDisk.entrySet()) {
       // use Long.max to avoid NaN
@@ -229,7 +251,8 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   @Override
-  public Map<String, Double> getAvgSizeOfEachReadForDisk() {
+  public synchronized Map<String, Double> getAvgSizeOfEachReadForDisk() {
+    checkUpdate();
     Map<String, Double> avgSizeOfReadMap = new HashMap<>(diskIdSet.size());
     for (Map.Entry<String, Long> readSectorSizeEntry : 
incrementReadSectorCountForDisk.entrySet()) {
       // use Long.max to avoid NaN
@@ -249,7 +272,8 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   @Override
-  public Map<String, Double> getAvgSizeOfEachWriteForDisk() {
+  public synchronized Map<String, Double> getAvgSizeOfEachWriteForDisk() {
+    checkUpdate();
     Map<String, Double> avgSizeOfWriteMap = new HashMap<>(diskIdSet.size());
     for (Map.Entry<String, Long> writeSectorSizeEntry :
         incrementWriteSectorCountForDisk.entrySet()) {
@@ -270,17 +294,20 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   @Override
-  public Map<String, Long> getMergedWriteOperationForDisk() {
-    return lastMergedWriteCountForDisk;
+  public synchronized Map<String, Long> getMergedWriteOperationForDisk() {
+    checkUpdate();
+    return new HashMap<>(lastMergedWriteCountForDisk);
   }
 
   @Override
-  public Map<String, Long> getMergedReadOperationForDisk() {
-    return lastMergedReadCountForDisk;
+  public synchronized Map<String, Long> getMergedReadOperationForDisk() {
+    checkUpdate();
+    return new HashMap<>(lastMergedReadCountForDisk);
   }
 
   @Override
-  public Map<String, Double> getQueueSizeForDisk() {
+  public synchronized Map<String, Double> getQueueSizeForDisk() {
+    checkUpdate();
     Map<String, Double> avgQueueSizeMap = new HashMap<>(diskIdSet.size());
     for (Map.Entry<String, Long> entry : 
incrementTimeInQueueForDisk.entrySet()) {
       avgQueueSizeMap.put(entry.getKey(), (((double) entry.getValue()) / 
updateInterval));
@@ -289,32 +316,38 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   @Override
-  public double getActualReadDataSizeForProcess() {
+  public synchronized double getActualReadDataSizeForProcess() {
+    checkUpdate();
     return lastReallyReadSizeForProcess / BYTES_PER_KB;
   }
 
   @Override
-  public double getActualWriteDataSizeForProcess() {
+  public synchronized double getActualWriteDataSizeForProcess() {
+    checkUpdate();
     return lastReallyWriteSizeForProcess / BYTES_PER_KB;
   }
 
   @Override
-  public long getReadOpsCountForProcess() {
+  public synchronized long getReadOpsCountForProcess() {
+    checkUpdate();
     return lastReadOpsCountForProcess;
   }
 
   @Override
-  public long getWriteOpsCountForProcess() {
+  public synchronized long getWriteOpsCountForProcess() {
+    checkUpdate();
     return lastWriteOpsCountForProcess;
   }
 
   @Override
-  public double getAttemptReadSizeForProcess() {
+  public synchronized double getAttemptReadSizeForProcess() {
+    checkUpdate();
     return lastAttemptReadSizeForProcess / BYTES_PER_KB;
   }
 
   @Override
-  public double getAttemptWriteSizeForProcess() {
+  public synchronized double getAttemptWriteSizeForProcess() {
+    checkUpdate();
     return lastAttemptWriteSizeForProcess / BYTES_PER_KB;
   }
 
@@ -324,7 +357,7 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   private void collectDiskId() {
-    File diskIdFolder = new File(DISK_ID_PATH);
+    File diskIdFolder = diskIdPath.toFile();
     if (!diskIdFolder.exists()) {
       return;
     }
@@ -338,8 +371,7 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
 
   private void collectDiskInfo() {
     for (String diskId : diskIdSet) {
-      String diskSectorSizePath = String.format(DISK_SECTOR_SIZE_PATH, diskId);
-      File diskSectorSizeFile = new File(diskSectorSizePath);
+      File diskSectorSizeFile = 
diskIdPath.resolve(diskId).resolve("queue/hw_sector_size").toFile();
       try (Scanner scanner = new 
Scanner(Files.newInputStream(diskSectorSizeFile.toPath()))) {
         if (scanner.hasNext()) {
           int sectorSize = Integer.parseInt(scanner.nextLine());
@@ -356,8 +388,7 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
     }
   }
 
-  private void updateInfo() {
-    long currentTime = System.currentTimeMillis();
+  private void updateInfo(long currentTime) {
     updateInterval = currentTime - lastUpdateTime;
     lastUpdateTime = currentTime;
     updateDiskInfo();
@@ -365,7 +396,7 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   private void updateDiskInfo() {
-    File diskStatsFile = new File(DISK_STATUS_FILE_PATH);
+    File diskStatsFile = diskStatsPath.toFile();
     if (!diskStatsFile.exists()) {
       LOGGER.warn(MetricsMessages.CANNOT_FIND_DISK_IO_STATUS_FILE, 
DISK_STATUS_FILE_PATH);
       return;
@@ -443,7 +474,7 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   private void updateProcessInfo() {
-    File processStatInfoFile = new File(processIoStatusPath);
+    File processStatInfoFile = processIoStatusPath.toFile();
     if (!processStatInfoFile.exists()) {
       LOGGER.warn(MetricsMessages.CANNOT_FIND_PROCESS_IO_STATUS_FILE, 
processIoStatusPath);
     }
@@ -472,8 +503,11 @@ public class LinuxDiskMetricsManager implements 
IDiskMetricsManager {
   }
 
   private void checkUpdate() {
-    if (System.currentTimeMillis() - lastUpdateTime > 
UPDATE_SMALLEST_INTERVAL) {
-      updateInfo();
+    // Accessors hold this monitor throughout refresh and snapshot 
construction. Reuse the checked
+    // timestamp so another clock read cannot turn a valid interval into zero 
or a negative value.
+    long currentTime = clock.getAsLong();
+    if (currentTime - lastUpdateTime > UPDATE_SMALLEST_INTERVAL) {
+      updateInfo(currentTime);
     }
   }
 }
diff --git 
a/iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/reporter/prometheus/PrometheusReporter.java
 
b/iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/reporter/prometheus/PrometheusReporter.java
index 30ce9573637..eb048dafbc8 100644
--- 
a/iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/reporter/prometheus/PrometheusReporter.java
+++ 
b/iotdb-core/metrics/interface/src/main/java/org/apache/iotdb/metrics/reporter/prometheus/PrometheusReporter.java
@@ -80,6 +80,8 @@ public class PrometheusReporter implements Reporter {
   private static final long PROMETHEUS_DEFAULT_SCRAPE_INTERVAL_SECONDS = 15;
   private static final int PROMETHEUS_RESPONSE_CHUNK_SIZE = 8 * 1024;
   private final AbstractMetricManager metricManager;
+  // Use a separate lock so stopping the HTTP server cannot wait on its own 
request threads.
+  private final Object scrapeLock = new Object();
   private final Supplier<ScheduledExecutorService> 
snapshotUpdateExecutorSupplier;
   private volatile ScheduledExecutorService snapshotUpdateExecutor;
   private volatile DisposableServer httpServer;
@@ -232,13 +234,7 @@ public class PrometheusReporter implements Reporter {
 
   private void updateSnapshot() {
     try {
-      String snapshot = scrape();
-      // Do not publish an empty scrape taken before metric sets are bound. 
The request path will
-      // synchronously scrape until the first complete snapshot is available. 
Empty snapshots are
-      // published after initialization so removed metrics are not kept in the 
cache indefinitely.
-      if (!snapshot.isEmpty() || metricsSnapshot != null) {
-        metricsSnapshot = snapshot;
-      }
+      scrape();
     } catch (Throwable t) {
       LOGGER.error(
           MetricsMessages
@@ -250,9 +246,12 @@ public class PrometheusReporter implements Reporter {
   private String getMetricsSnapshot() {
     String snapshot = metricsSnapshot;
     if (snapshot == null) {
-      snapshot = scrape();
-      if (!snapshot.isEmpty()) {
-        metricsSnapshot = snapshot;
+      synchronized (scrapeLock) {
+        // The background updater or another first request may have populated 
the cache meanwhile.
+        snapshot = metricsSnapshot;
+        if (snapshot == null) {
+          snapshot = scrape();
+        }
       }
     }
     return snapshot;
@@ -311,6 +310,20 @@ public class PrometheusReporter implements Reporter {
   }
 
   private String scrape() {
+    synchronized (scrapeLock) {
+      // Metric suppliers may update shared sampling state. Serialize all 
scrapes, including the
+      // first-request fallback and synchronous mode, and publish in the same 
order as collection.
+      String snapshot = collectMetrics();
+      // Do not cache an empty scrape before metric sets are bound. After 
initialization, publish
+      // empty snapshots too so removed metrics are not retained indefinitely.
+      if (!snapshot.isEmpty() || metricsSnapshot != null) {
+        metricsSnapshot = snapshot;
+      }
+      return snapshot;
+    }
+  }
+
+  private String collectMetrics() {
     Writer writer = new StringWriter();
     PrometheusTextWriter prometheusTextWriter = new 
PrometheusTextWriter(writer);
 
diff --git 
a/iotdb-core/metrics/interface/src/test/java/org/apache/iotdb/metrics/metricsets/disk/LinuxDiskMetricsManagerTest.java
 
b/iotdb-core/metrics/interface/src/test/java/org/apache/iotdb/metrics/metricsets/disk/LinuxDiskMetricsManagerTest.java
new file mode 100644
index 00000000000..3463fcf4d0a
--- /dev/null
+++ 
b/iotdb-core/metrics/interface/src/test/java/org/apache/iotdb/metrics/metricsets/disk/LinuxDiskMetricsManagerTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.iotdb.metrics.metricsets.disk;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.LongSupplier;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class LinuxDiskMetricsManagerTest {
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  private Path diskStats;
+  private Path diskIds;
+  private Path processIo;
+  private final AtomicLong clock = new AtomicLong(10001);
+
+  @Before
+  public void setUp() throws Exception {
+    diskStats = temporaryFolder.newFile("diskstats").toPath();
+    diskIds = temporaryFolder.newFolder("block").toPath();
+    Path queue = Files.createDirectories(diskIds.resolve("vda/queue"));
+    Files.write(queue.resolve("hw_sector_size"), 
"512\n".getBytes(StandardCharsets.UTF_8));
+    processIo = temporaryFolder.newFile("io").toPath();
+    Files.write(processIo, "syscr: 1\nsyscw: 
2\n".getBytes(StandardCharsets.UTF_8));
+    writeDiskStats(10, 100, 200);
+  }
+
+  @Test
+  public void testFirstSampleAndUnchangedOrBackwardClock() throws Exception {
+    LinuxDiskMetricsManager manager = newManager(clock::get);
+    assertEquals(0.0, manager.getIoUtilsPercentage().get("vda"), 0.0);
+    assertEquals(0.0, manager.getQueueSizeForDisk().get("vda"), 0.0);
+
+    clock.addAndGet(20000);
+    writeDiskStats(20, 5100, 10200);
+    assertEquals(0.25, manager.getIoUtilsPercentage().get("vda"), 0.0);
+    assertEquals(0.5, manager.getQueueSizeForDisk().get("vda"), 0.0);
+    Map<String, Long> previousCounts = manager.getReadOperationCountForDisk();
+
+    writeDiskStats(30, 15100, 30200);
+    // Neither a zero interval nor a backward wall clock replaces the last 
valid sample.
+    assertEquals(0.25, manager.getIoUtilsPercentage().get("vda"), 0.0);
+    clock.set(1);
+    assertEquals(0.5, manager.getQueueSizeForDisk().get("vda"), 0.0);
+    clock.set(50001);
+    assertEquals(0.5, manager.getIoUtilsPercentage().get("vda"), 0.0);
+    assertEquals(1.0, manager.getQueueSizeForDisk().get("vda"), 0.0);
+    assertEquals(Long.valueOf(20), previousCounts.get("vda"));
+    assertEquals(Long.valueOf(30), 
manager.getReadOperationCountForDisk().get("vda"));
+  }
+
+  @Test
+  public void testRefreshUsesTheTimestampCheckedForEligibility() {
+    // A second clock read would return the previous sample time and cause a 
zero denominator.
+    AtomicLong nextTime = new AtomicLong(10001);
+    LinuxDiskMetricsManager manager = newManager(() -> 
nextTime.getAndSet(10001));
+    manager.getReadDataSizeForDisk();
+    nextTime.set(30001);
+    manager.getReadDataSizeForDisk();
+    assertEquals(0.0, manager.getIoUtilsPercentage().get("vda"), 0.0);
+    assertEquals(0.0, manager.getQueueSizeForDisk().get("vda"), 0.0);
+  }
+
+  @Test
+  public void testConcurrentReadersWaitForCompleteSample() throws Exception {
+    CountDownLatch sampling = new CountDownLatch(1);
+    CountDownLatch release = new CountDownLatch(1);
+    LinuxDiskMetricsManager manager =
+        newManager(
+            () -> {
+              sampling.countDown();
+              try {
+                assertTrue(release.await(10, TimeUnit.SECONDS));
+              } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                throw new AssertionError(e);
+              }
+              return clock.get();
+            });
+    FutureTask<Map<String, Double>> refresh = new 
FutureTask<>(manager::getReadDataSizeForDisk);
+    FutureTask<Map<String, Double>> read = new 
FutureTask<>(manager::getIoUtilsPercentage);
+    Thread refreshing = new Thread(refresh, "disk-refresh");
+    Thread reading = new Thread(read, "disk-read");
+    try {
+      refreshing.start();
+      assertTrue(sampling.await(10, TimeUnit.SECONDS));
+      reading.start();
+      long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+      while (reading.getState() != Thread.State.BLOCKED && !read.isDone()) {
+        if (System.nanoTime() > deadline) {
+          fail("The metric reader did not wait for the active disk sample");
+        }
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
+      }
+      assertEquals(Thread.State.BLOCKED, reading.getState());
+      release.countDown();
+      refresh.get(10, TimeUnit.SECONDS);
+      assertEquals(0.0, read.get(10, TimeUnit.SECONDS).get("vda"), 0.0);
+    } finally {
+      release.countDown();
+      refresh.cancel(true);
+      read.cancel(true);
+      refreshing.join(TimeUnit.SECONDS.toMillis(10));
+      reading.join(TimeUnit.SECONDS.toMillis(10));
+    }
+  }
+
+  private LinuxDiskMetricsManager newManager(LongSupplier timeSupplier) {
+    return new LinuxDiskMetricsManager(diskStats, diskIds, processIo, 
timeSupplier);
+  }
+
+  private void writeDiskStats(long reads, long busyTime, long queueTime) 
throws Exception {
+    String stats = "   8 0 vda " + reads + " 0 40 0 0 0 0 0 0 " + busyTime + " 
" + queueTime + "\n";
+    Files.write(diskStats, stats.getBytes(StandardCharsets.UTF_8));
+  }
+}
diff --git 
a/iotdb-core/metrics/interface/src/test/java/org/apache/iotdb/metrics/reporter/prometheus/PrometheusReporterTest.java
 
b/iotdb-core/metrics/interface/src/test/java/org/apache/iotdb/metrics/reporter/prometheus/PrometheusReporterTest.java
index 5b19d7f5f31..4ad8ddc1350 100644
--- 
a/iotdb-core/metrics/interface/src/test/java/org/apache/iotdb/metrics/reporter/prometheus/PrometheusReporterTest.java
+++ 
b/iotdb-core/metrics/interface/src/test/java/org/apache/iotdb/metrics/reporter/prometheus/PrometheusReporterTest.java
@@ -21,24 +21,39 @@ package org.apache.iotdb.metrics.reporter.prometheus;
 
 import org.apache.iotdb.metrics.config.MetricConfig;
 import org.apache.iotdb.metrics.config.MetricConfigDescriptor;
+import org.apache.iotdb.metrics.impl.DoNothingAutoGauge;
 import org.apache.iotdb.metrics.impl.DoNothingMetricManager;
+import org.apache.iotdb.metrics.type.AutoGauge;
+import org.apache.iotdb.metrics.type.IMetric;
+import org.apache.iotdb.metrics.utils.MetricInfo;
+import org.apache.iotdb.metrics.utils.MetricType;
 
 import org.junit.Test;
 import reactor.netty.DisposableServer;
 
 import java.io.InputStream;
 import java.lang.reflect.Field;
+import java.lang.reflect.Method;
 import java.net.HttpURLConnection;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
 import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.LockSupport;
+import java.util.function.DoubleSupplier;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class PrometheusReporterTest {
 
@@ -79,6 +94,47 @@ public class PrometheusReporterTest {
     }
   }
 
+  @Test
+  public void testFirstRequestWaitsForBackgroundSnapshot() throws Exception {
+    assertSerializedScrapes("updateSnapshot", "getMetricsSnapshot", 1);
+  }
+
+  @Test
+  public void testConcurrentFirstRequestsReuseSnapshot() throws Exception {
+    assertSerializedScrapes("getMetricsSnapshot", "getMetricsSnapshot", 1);
+  }
+
+  @Test
+  public void testBackgroundRefreshWaitsForFirstRequest() throws Exception {
+    assertSerializedScrapes("getMetricsSnapshot", "updateSnapshot", 2);
+  }
+
+  @Test
+  public void testSynchronousScrapeWaitsForFirstRequest() throws Exception {
+    assertSerializedScrapes("getMetricsSnapshot", "scrape", 2);
+  }
+
+  @Test
+  public void testEmptySnapshotBeforeAndAfterMetricRegistration() throws 
Exception {
+    Map<MetricInfo, IMetric> registeredMetrics = new HashMap<>();
+    PrometheusReporter reporter =
+        new PrometheusReporter(
+            new DoNothingMetricManager() {
+              @Override
+              public Map<MetricInfo, IMetric> getAllMetrics() {
+                return registeredMetrics;
+              }
+            });
+    assertEquals("", invoke(reporter, "getMetricsSnapshot"));
+    registeredMetrics.put(new MetricInfo(MetricType.AUTO_GAUGE, "sample"), 
sampleGauge(() -> 1.0));
+    String snapshot = (String) invoke(reporter, "getMetricsSnapshot");
+    assertTrue(snapshot.contains(" 1.0\n"));
+    registeredMetrics.clear();
+    assertEquals(snapshot, invoke(reporter, "getMetricsSnapshot"));
+    invoke(reporter, "updateSnapshot");
+    assertEquals("", invoke(reporter, "getMetricsSnapshot"));
+  }
+
   @Test
   public void testMetricsResponseChunksPreserveContentAndSurrogatePairs() {
     String metrics = "x".repeat(8 * 1024 - 1) + "\uD83D\uDE00" + "y".repeat(8 
* 1024 + 1);
@@ -129,4 +185,79 @@ public class PrometheusReporterTest {
       metricConfig.setPrometheusReporterPort(originalPort);
     }
   }
+
+  private void assertSerializedScrapes(String firstMethod, String 
secondMethod, int expectedReads)
+      throws Exception {
+    CountDownLatch collecting = new CountDownLatch(1);
+    CountDownLatch release = new CountDownLatch(1);
+    AtomicInteger reads = new AtomicInteger();
+    DoubleSupplier supplier =
+        () -> {
+          int value = reads.incrementAndGet();
+          if (value == 1) {
+            collecting.countDown();
+            try {
+              assertTrue(release.await(10, TimeUnit.SECONDS));
+            } catch (InterruptedException e) {
+              Thread.currentThread().interrupt();
+              throw new AssertionError(e);
+            }
+          }
+          return value;
+        };
+    PrometheusReporter reporter =
+        new PrometheusReporter(
+            new DoNothingMetricManager() {
+              @Override
+              public Map<MetricInfo, IMetric> getAllMetrics() {
+                return Collections.singletonMap(
+                    new MetricInfo(MetricType.AUTO_GAUGE, "sample"), 
sampleGauge(supplier));
+              }
+            });
+    FutureTask<Object> first = new FutureTask<>(() -> invoke(reporter, 
firstMethod));
+    FutureTask<Object> second = new FutureTask<>(() -> invoke(reporter, 
secondMethod));
+    Thread firstThread = new Thread(first, "first-scrape");
+    Thread secondThread = new Thread(second, "second-scrape");
+    try {
+      firstThread.start();
+      assertTrue(collecting.await(10, TimeUnit.SECONDS));
+      secondThread.start();
+      long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+      while (secondThread.getState() != Thread.State.BLOCKED && 
!second.isDone()) {
+        if (System.nanoTime() > deadline) {
+          fail("The second scrape did not wait for the active collection");
+        }
+        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
+      }
+      assertEquals(Thread.State.BLOCKED, secondThread.getState());
+      assertEquals(1, reads.get());
+      release.countDown();
+      first.get(10, TimeUnit.SECONDS);
+      second.get(10, TimeUnit.SECONDS);
+      String snapshot = (String) invoke(reporter, "getMetricsSnapshot");
+      assertTrue(snapshot.endsWith(" " + expectedReads + ".0\n"));
+      assertEquals(expectedReads, reads.get());
+    } finally {
+      release.countDown();
+      first.cancel(true);
+      second.cancel(true);
+      firstThread.join(TimeUnit.SECONDS.toMillis(10));
+      secondThread.join(TimeUnit.SECONDS.toMillis(10));
+    }
+  }
+
+  private static AutoGauge sampleGauge(DoubleSupplier supplier) {
+    return new DoNothingAutoGauge() {
+      @Override
+      public double getValue() {
+        return supplier.getAsDouble();
+      }
+    };
+  }
+
+  private static Object invoke(PrometheusReporter reporter, String methodName) 
throws Exception {
+    Method method = PrometheusReporter.class.getDeclaredMethod(methodName);
+    method.setAccessible(true);
+    return method.invoke(reporter);
+  }
 }

Reply via email to