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

Caideyipi pushed a commit to branch fix/subscription-stall-observability
in repository https://gitbox.apache.org/repos/asf/iotdb.git

commit e3dc70598bef5cf3ae8106c72d3ed69dcf10d882
Author: Caideyipi <[email protected]>
AuthorDate: Fri Oct 9 14:14:07 2026 +0800

    Add subscription delivery stall and memory overcommit diagnostics
---
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |   8 +
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |   8 +
 .../consensus/ConsensusPrefetchingQueue.java       |  64 ++++++++
 .../consensus/SubscriptionQueueTimeTracker.java    | 115 +++++++++++++
 ...nsensusSubscriptionPrefetchingQueueMetrics.java |  60 +++++++
 .../subscription/metric/SubscriptionMetrics.java   |  34 ++++
 .../resource/SubscriptionMemoryManager.java        |  37 +++++
 ...onsensusPrefetchingQueueDataNodeMemoryTest.java | 180 ++++++++++++++++++++-
 .../metric/SubscriptionMetricsRestartTest.java     |  87 ++++++++++
 .../resource/SubscriptionMemoryManagerTest.java    |  12 ++
 .../iotdb/commons/service/metric/enums/Metric.java |  13 ++
 11 files changed, 611 insertions(+), 7 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index 1c0362b627b..e4f487f3fac 100644
--- 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -21,6 +21,14 @@ package org.apache.iotdb.db.i18n;
 
 public final class DataNodePipeMessages {
 
+  public static final String
+      
LOG_SUBSCRIPTION_MEMORY_OVERCOMMIT_ENTRYBYTES_ARG_BUDGETBYTES_ARG_OVERCOMMITBYTES_ARG_OTHER_QUEUES_MAY_BE_BLOCKED_UNTIL_RELEASE_DF9B914E
 =
+          "Subscription memory overcommit: entryBytes={}, budgetBytes={}, 
overcommitBytes={}; other queues may be blocked until release.";
+
+  public static final String
+      
LOG_SUBSCRIPTION_PREFETCH_ROUND_TOOK_ARG_MS_FOR_ARG_DELIVERYIDLETIMEMS_ARG_MEMORYUSEDBYTES_ARG_MEMORYLIMITBYTES_ARG_INSPECT_WORKER_STACKS_WAL_I_O_AND_JVM_PAUSES_D399CEDD
 =
+          "Subscription prefetch round took {} ms for {}. 
deliveryIdleTimeMs={}, memoryUsedBytes={}, memoryLimitBytes={}; inspect worker 
stacks, WAL I/O and JVM pauses.";
+
   public static final String LOG_FAILED_TO_RESOLVE_TRANSFER_EXCEPTION_A4F5397A 
=
       "Failed to resolve transfer exception.";
 
diff --git 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
index 49096659ade..2a94519e41c 100644
--- 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodePipeMessages.java
@@ -21,6 +21,14 @@ package org.apache.iotdb.db.i18n;
 
 public final class DataNodePipeMessages {
 
+  public static final String
+      
LOG_SUBSCRIPTION_MEMORY_OVERCOMMIT_ENTRYBYTES_ARG_BUDGETBYTES_ARG_OVERCOMMITBYTES_ARG_OTHER_QUEUES_MAY_BE_BLOCKED_UNTIL_RELEASE_DF9B914E
 =
+          
"订阅内存超出预算:entryBytes={},budgetBytes={},overcommitBytes={};其它队列可能阻塞,直到该 entry 
释放。";
+
+  public static final String
+      
LOG_SUBSCRIPTION_PREFETCH_ROUND_TOOK_ARG_MS_FOR_ARG_DELIVERYIDLETIMEMS_ARG_MEMORYUSEDBYTES_ARG_MEMORYLIMITBYTES_ARG_INSPECT_WORKER_STACKS_WAL_I_O_AND_JVM_PAUSES_D399CEDD
 =
+          "订阅预取轮次耗时 {} ms,队列 
{}。deliveryIdleTimeMs={},memoryUsedBytes={},memoryLimitBytes={};请检查工作线程栈、WAL 
I/O 和 JVM 停顿。";
+
   public static final String LOG_FAILED_TO_RESOLVE_TRANSFER_EXCEPTION_A4F5397A 
=
       "解析 transfer exception 失败。";
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
index b692ec94ed0..a2a3871080e 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueue.java
@@ -285,6 +285,8 @@ public class ConsensusPrefetchingQueue {
 
   private volatile long lastProgressTimeMs = 0L;
 
+  private final SubscriptionQueueTimeTracker timeTracker = new 
SubscriptionQueueTimeTracker();
+
   private volatile String lastConsumerId = "";
 
   private volatile long lastPendingAcceptedEntries = 0L;
@@ -838,6 +840,7 @@ public class ConsensusPrefetchingQueue {
     this.subscriptionWALIterator =
         createSubscriptionWALIterator(resolvedStart.getStartSearchIndex());
     this.prefetchInitialized = true;
+    timeTracker.start();
     this.observedSeekGeneration = seekGeneration.get();
     discardBatch(this.lingerBatch);
     resetBatchWriterProgress();
@@ -1321,6 +1324,7 @@ public class ConsensusPrefetchingQueue {
 
         // Mark as polled before updating inFlightEvents
         event.recordLastPolledTimestamp();
+        timeTracker.recordDelivery();
         lastPollTimeMs = System.currentTimeMillis();
         lastConsumerId = consumerId;
         inFlightEvents.put(new InFlightEventKey(consumerId, 
event.getCommitContext()), event);
@@ -1406,7 +1410,28 @@ public class ConsensusPrefetchingQueue {
 
   private static final long PREFETCH_STATS_LOG_INTERVAL_MS = 5_000L;
 
+  private static final long SLOW_PREFETCH_ROUND_THRESHOLD_MS = 10_000L;
+
   public PrefetchRoundResult drivePrefetchOnce() {
+    timeTracker.beginPrefetch();
+    try {
+      return drivePrefetchOnceInternal();
+    } finally {
+      final long durationMs = timeTracker.endPrefetch();
+      if (durationMs >= SLOW_PREFETCH_ROUND_THRESHOLD_MS) {
+        LOGGER.warn(
+            DataNodePipeMessages
+                
.LOG_SUBSCRIPTION_PREFETCH_ROUND_TOOK_ARG_MS_FOR_ARG_DELIVERYIDLETIMEMS_ARG_MEMORYUSEDBYTES_ARG_MEMORYLIMITBYTES_ARG_INSPECT_WORKER_STACKS_WAL_I_O_AND_JVM_PAUSES_D399CEDD,
+            durationMs,
+            this,
+            getDeliveryIdleTimeMs(),
+            subscriptionMemoryManager.getUsedMemorySizeInBytes(),
+            subscriptionMemoryManager.getTotalMemorySizeInBytes());
+      }
+    }
+  }
+
+  private PrefetchRoundResult drivePrefetchOnceInternal() {
     if (applyPendingSeekRequestIfNecessary()) {
       return closeRequested ? PrefetchRoundResult.dormant() : 
PrefetchRoundResult.rescheduleNow();
     }
@@ -4135,6 +4160,30 @@ public class ConsensusPrefetchingQueue {
     return lastProgressTimeMs;
   }
 
+  public long getLastDeliveryIntervalMs() {
+    return timeTracker.getLastDeliveryIntervalMs();
+  }
+
+  public long getMaxDeliveryIntervalMs() {
+    return timeTracker.getMaxDeliveryIntervalMs();
+  }
+
+  public long getDeliveryIdleTimeMs() {
+    return timeTracker.getDeliveryIdleTimeMs();
+  }
+
+  public long getPrefetchDurationMs() {
+    return timeTracker.getPrefetchDurationMs();
+  }
+
+  public long getMaxPrefetchDurationMs() {
+    return timeTracker.getMaxPrefetchDurationMs();
+  }
+
+  public long getPrefetchIdleTimeMs() {
+    return timeTracker.getPrefetchIdleTimeMs();
+  }
+
   /** Returns 0=uninitialized, 1=inactive, 2=caught up, 3=catching up, 
4=stalled. */
   public long getProgressStatus() {
     switch (getProgressStatusName()) {
@@ -4467,6 +4516,21 @@ public class ConsensusPrefetchingQueue {
     result.put("retainedRequestBytes", 
String.valueOf(getRetainedRequestBytes()));
     result.put("retainedTabletBytes", 
String.valueOf(retainedTabletBytes.get()));
     result.put("memoryBlockedEntryBytes", 
String.valueOf(memoryBlockedEntryBytes));
+    result.put(
+        "subscriptionMemoryUsedInBytes",
+        String.valueOf(subscriptionMemoryManager.getUsedMemorySizeInBytes()));
+    result.put(
+        "subscriptionMemoryOvercommitBytes",
+        String.valueOf(subscriptionMemoryManager.getOvercommitSizeInBytes()));
+    result.put(
+        "subscriptionMemoryOversizedEntryCount",
+        String.valueOf(subscriptionMemoryManager.getOversizedEntryCount()));
+    result.put("lastDeliveryIntervalMs", 
String.valueOf(getLastDeliveryIntervalMs()));
+    result.put("maxDeliveryIntervalMs", 
String.valueOf(getMaxDeliveryIntervalMs()));
+    result.put("deliveryIdleTimeMs", String.valueOf(getDeliveryIdleTimeMs()));
+    result.put("prefetchDurationMs", String.valueOf(getPrefetchDurationMs()));
+    result.put("maxPrefetchDurationMs", 
String.valueOf(getMaxPrefetchDurationMs()));
+    result.put("prefetchIdleTimeMs", String.valueOf(getPrefetchIdleTimeMs()));
     result.put("realtimeAdmissionBlocked", 
String.valueOf(realtimeAdmissionBlocked.get()));
     result.put(
         "subscriptionMemoryLimitInBytes",
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/SubscriptionQueueTimeTracker.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/SubscriptionQueueTimeTracker.java
new file mode 100644
index 00000000000..350759b1205
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/SubscriptionQueueTimeTracker.java
@@ -0,0 +1,115 @@
+/*
+ * 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.subscription.broker.consensus;
+
+import org.apache.iotdb.commons.utils.TestOnly;
+
+import java.util.concurrent.TimeUnit;
+import java.util.function.LongSupplier;
+
+/** Monotonic timing that remains readable while a queue's prefetch worker is 
blocked. */
+class SubscriptionQueueTimeTracker {
+
+  private final LongSupplier nanoTime;
+
+  private volatile boolean started;
+  private boolean delivered;
+  private volatile long lastDeliveryTimeNs;
+  private volatile long lastDeliveryIntervalMs;
+  private volatile long maxDeliveryIntervalMs;
+  private volatile long lastPrefetchCompletionTimeNs;
+  private volatile long prefetchStartTimeNs;
+  private volatile boolean prefetchRunning;
+  private volatile long maxPrefetchDurationMs;
+
+  SubscriptionQueueTimeTracker() {
+    this(System::nanoTime);
+  }
+
+  @TestOnly
+  SubscriptionQueueTimeTracker(final LongSupplier nanoTime) {
+    this.nanoTime = nanoTime;
+  }
+
+  synchronized void start() {
+    if (!started) {
+      final long nowNs = nanoTime.getAsLong();
+      lastDeliveryTimeNs = nowNs;
+      lastPrefetchCompletionTimeNs = nowNs;
+      started = true;
+    }
+  }
+
+  synchronized void recordDelivery() {
+    start();
+    final long nowNs = nanoTime.getAsLong();
+    if (delivered) {
+      lastDeliveryIntervalMs = elapsedMs(nowNs, lastDeliveryTimeNs);
+      maxDeliveryIntervalMs = Math.max(maxDeliveryIntervalMs, 
lastDeliveryIntervalMs);
+    }
+    lastDeliveryTimeNs = nowNs;
+    delivered = true;
+  }
+
+  synchronized void beginPrefetch() {
+    start();
+    prefetchStartTimeNs = nanoTime.getAsLong();
+    prefetchRunning = true;
+  }
+
+  synchronized long endPrefetch() {
+    final long nowNs = nanoTime.getAsLong();
+    final long durationMs = elapsedMs(nowNs, prefetchStartTimeNs);
+    maxPrefetchDurationMs = Math.max(maxPrefetchDurationMs, durationMs);
+    lastPrefetchCompletionTimeNs = nowNs;
+    prefetchRunning = false;
+    return durationMs;
+  }
+
+  long getLastDeliveryIntervalMs() {
+    return lastDeliveryIntervalMs;
+  }
+
+  long getMaxDeliveryIntervalMs() {
+    return maxDeliveryIntervalMs;
+  }
+
+  long getDeliveryIdleTimeMs() {
+    return started ? elapsedMs(nanoTime.getAsLong(), lastDeliveryTimeNs) : 0L;
+  }
+
+  /** Elapsed time of an ongoing round, including waiting to acquire the queue 
lock. */
+  long getPrefetchDurationMs() {
+    return prefetchRunning ? elapsedMs(nanoTime.getAsLong(), 
prefetchStartTimeNs) : 0L;
+  }
+
+  long getMaxPrefetchDurationMs() {
+    return Math.max(maxPrefetchDurationMs, getPrefetchDurationMs());
+  }
+
+  /** Time since the last completed round, or activation before the first 
completed round. */
+  long getPrefetchIdleTimeMs() {
+    return started ? elapsedMs(nanoTime.getAsLong(), 
lastPrefetchCompletionTimeNs) : 0L;
+  }
+
+  private static long elapsedMs(final long nowNs, final long thenNs) {
+    return Math.max(0L, TimeUnit.NANOSECONDS.toMillis(nowNs - thenNs));
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/ConsensusSubscriptionPrefetchingQueueMetrics.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/ConsensusSubscriptionPrefetchingQueueMetrics.java
index 19076e9abae..27325ab602b 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/ConsensusSubscriptionPrefetchingQueueMetrics.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/ConsensusSubscriptionPrefetchingQueueMetrics.java
@@ -193,6 +193,42 @@ public class ConsensusSubscriptionPrefetchingQueueMetrics 
implements IMetricSet
         queue,
         ConsensusPrefetchingQueue::getLastProgressTimeMs,
         key.getTags());
+    metricService.createAutoGauge(
+        Metric.SUBSCRIPTION_CONSENSUS_LAST_DELIVERY_INTERVAL_MS.toString(),
+        MetricLevel.IMPORTANT,
+        queue,
+        ConsensusPrefetchingQueue::getLastDeliveryIntervalMs,
+        key.getTags());
+    metricService.createAutoGauge(
+        Metric.SUBSCRIPTION_CONSENSUS_MAX_DELIVERY_INTERVAL_MS.toString(),
+        MetricLevel.IMPORTANT,
+        queue,
+        ConsensusPrefetchingQueue::getMaxDeliveryIntervalMs,
+        key.getTags());
+    metricService.createAutoGauge(
+        Metric.SUBSCRIPTION_CONSENSUS_DELIVERY_IDLE_TIME_MS.toString(),
+        MetricLevel.IMPORTANT,
+        queue,
+        ConsensusPrefetchingQueue::getDeliveryIdleTimeMs,
+        key.getTags());
+    metricService.createAutoGauge(
+        Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_DURATION_MS.toString(),
+        MetricLevel.IMPORTANT,
+        queue,
+        ConsensusPrefetchingQueue::getPrefetchDurationMs,
+        key.getTags());
+    metricService.createAutoGauge(
+        Metric.SUBSCRIPTION_CONSENSUS_MAX_PREFETCH_DURATION_MS.toString(),
+        MetricLevel.IMPORTANT,
+        queue,
+        ConsensusPrefetchingQueue::getMaxPrefetchDurationMs,
+        key.getTags());
+    metricService.createAutoGauge(
+        Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_IDLE_TIME_MS.toString(),
+        MetricLevel.IMPORTANT,
+        queue,
+        ConsensusPrefetchingQueue::getPrefetchIdleTimeMs,
+        key.getTags());
     metricService.createAutoGauge(
         Metric.SUBSCRIPTION_CONSENSUS_PROGRESS_STATUS.toString(),
         MetricLevel.IMPORTANT,
@@ -305,6 +341,30 @@ public class ConsensusSubscriptionPrefetchingQueueMetrics 
implements IMetricSet
         MetricType.AUTO_GAUGE,
         Metric.SUBSCRIPTION_CONSENSUS_LAST_PROGRESS_TIME.toString(),
         key.getTags());
+    metricService.remove(
+        MetricType.AUTO_GAUGE,
+        Metric.SUBSCRIPTION_CONSENSUS_LAST_DELIVERY_INTERVAL_MS.toString(),
+        key.getTags());
+    metricService.remove(
+        MetricType.AUTO_GAUGE,
+        Metric.SUBSCRIPTION_CONSENSUS_MAX_DELIVERY_INTERVAL_MS.toString(),
+        key.getTags());
+    metricService.remove(
+        MetricType.AUTO_GAUGE,
+        Metric.SUBSCRIPTION_CONSENSUS_DELIVERY_IDLE_TIME_MS.toString(),
+        key.getTags());
+    metricService.remove(
+        MetricType.AUTO_GAUGE,
+        Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_DURATION_MS.toString(),
+        key.getTags());
+    metricService.remove(
+        MetricType.AUTO_GAUGE,
+        Metric.SUBSCRIPTION_CONSENSUS_MAX_PREFETCH_DURATION_MS.toString(),
+        key.getTags());
+    metricService.remove(
+        MetricType.AUTO_GAUGE,
+        Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_IDLE_TIME_MS.toString(),
+        key.getTags());
     metricService.remove(
         MetricType.AUTO_GAUGE,
         Metric.SUBSCRIPTION_CONSENSUS_PROGRESS_STATUS.toString(),
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetrics.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetrics.java
index 29de59ddf32..8e8657bb3d5 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetrics.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetrics.java
@@ -19,8 +19,13 @@
 
 package org.apache.iotdb.db.subscription.metric;
 
+import org.apache.iotdb.commons.service.metric.enums.Metric;
+import 
org.apache.iotdb.db.subscription.resource.SubscriptionDataNodeResourceManager;
+import org.apache.iotdb.db.subscription.resource.SubscriptionMemoryManager;
 import org.apache.iotdb.metrics.AbstractMetricService;
 import org.apache.iotdb.metrics.metricsets.IMetricSet;
+import org.apache.iotdb.metrics.utils.MetricLevel;
+import org.apache.iotdb.metrics.utils.MetricType;
 
 public class SubscriptionMetrics implements IMetricSet {
 
@@ -30,12 +35,41 @@ public class SubscriptionMetrics implements IMetricSet {
   public void bindTo(final AbstractMetricService metricService) {
     SubscriptionPrefetchingQueueMetrics.getInstance().bindTo(metricService);
     
ConsensusSubscriptionPrefetchingQueueMetrics.getInstance().bindTo(metricService);
+    // Read the shared allocator directly; queue snapshots taken at different 
times cannot be
+    // summed to establish whether a DataNode exceeded its subscription memory 
budget.
+    final SubscriptionMemoryManager memoryManager = 
SubscriptionDataNodeResourceManager.memory();
+    metricService.createAutoGauge(
+        Metric.SUBSCRIPTION_MEMORY_USED_BYTES.toString(),
+        MetricLevel.IMPORTANT,
+        memoryManager,
+        SubscriptionMemoryManager::getUsedMemorySizeInBytes);
+    metricService.createAutoGauge(
+        Metric.SUBSCRIPTION_MEMORY_LIMIT_BYTES.toString(),
+        MetricLevel.IMPORTANT,
+        memoryManager,
+        SubscriptionMemoryManager::getTotalMemorySizeInBytes);
+    metricService.createAutoGauge(
+        Metric.SUBSCRIPTION_MEMORY_OVERCOMMIT_BYTES.toString(),
+        MetricLevel.IMPORTANT,
+        memoryManager,
+        SubscriptionMemoryManager::getOvercommitSizeInBytes);
+    metricService.createAutoGauge(
+        Metric.SUBSCRIPTION_MEMORY_OVERSIZED_ENTRY_COUNT.toString(),
+        MetricLevel.IMPORTANT,
+        memoryManager,
+        SubscriptionMemoryManager::getOversizedEntryCount);
   }
 
   @Override
   public void unbindFrom(final AbstractMetricService metricService) {
     
SubscriptionPrefetchingQueueMetrics.getInstance().unbindFrom(metricService);
     
ConsensusSubscriptionPrefetchingQueueMetrics.getInstance().unbindFrom(metricService);
+    metricService.remove(MetricType.AUTO_GAUGE, 
Metric.SUBSCRIPTION_MEMORY_USED_BYTES.toString());
+    metricService.remove(MetricType.AUTO_GAUGE, 
Metric.SUBSCRIPTION_MEMORY_LIMIT_BYTES.toString());
+    metricService.remove(
+        MetricType.AUTO_GAUGE, 
Metric.SUBSCRIPTION_MEMORY_OVERCOMMIT_BYTES.toString());
+    metricService.remove(
+        MetricType.AUTO_GAUGE, 
Metric.SUBSCRIPTION_MEMORY_OVERSIZED_ENTRY_COUNT.toString());
   }
 
   //////////////////////////// singleton ////////////////////////////
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManager.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManager.java
index bf71fad6c4b..e33053baa2c 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManager.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManager.java
@@ -24,13 +24,27 @@ import org.apache.iotdb.commons.memory.IMemoryBlock;
 import org.apache.iotdb.commons.memory.MemoryBlockType;
 import org.apache.iotdb.commons.utils.TestOnly;
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.i18n.DataNodePipeMessages;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.TimeUnit;
 
 public class SubscriptionMemoryManager {
 
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(SubscriptionMemoryManager.class);
+
   private static final String MEMORY_BLOCK_NAME = "Subscription";
 
+  private static final long OVERCOMMIT_WARNING_INTERVAL_NS = 
TimeUnit.SECONDS.toNanos(30);
+
   private final IMemoryBlock memoryBlock;
 
+  private volatile long oversizedEntryCount = 0L;
+
+  private long lastOvercommitWarningTimeNs;
+
   SubscriptionMemoryManager() {
     memoryBlock =
         IoTDBDescriptor.getInstance()
@@ -52,6 +66,9 @@ public class SubscriptionMemoryManager {
    * <p>A single entry larger than the whole budget is allowed only while the 
budget is otherwise
    * empty. This avoids permanently blocking progress while keeping the 
overrun bounded by one
    * consensus entry.
+   *
+   * <p>This is a soft limit for such entries. Other queues can be blocked 
until the oversized entry
+   * is released; the exception is exposed through metrics and a rate-limited 
warning.
    */
   public synchronized boolean tryAllocate(final long sizeInBytes) {
     if (sizeInBytes <= 0L) {
@@ -64,6 +81,18 @@ public class SubscriptionMemoryManager {
         && memoryBlock.getUsedMemoryInBytes() == 0L
         && sizeInBytes > memoryBlock.getTotalMemorySizeInBytes()) {
       memoryBlock.forceAllocateWithoutLimitation(sizeInBytes);
+      oversizedEntryCount++;
+      final long nowNs = System.nanoTime();
+      if (oversizedEntryCount == 1L
+          || nowNs - lastOvercommitWarningTimeNs >= 
OVERCOMMIT_WARNING_INTERVAL_NS) {
+        lastOvercommitWarningTimeNs = nowNs;
+        LOGGER.warn(
+            DataNodePipeMessages
+                
.LOG_SUBSCRIPTION_MEMORY_OVERCOMMIT_ENTRYBYTES_ARG_BUDGETBYTES_ARG_OVERCOMMITBYTES_ARG_OTHER_QUEUES_MAY_BE_BLOCKED_UNTIL_RELEASE_DF9B914E,
+            sizeInBytes,
+            memoryBlock.getTotalMemorySizeInBytes(),
+            getOvercommitSizeInBytes());
+      }
       return true;
     }
     return false;
@@ -86,4 +115,12 @@ public class SubscriptionMemoryManager {
   public long getFreeMemorySizeInBytes() {
     return memoryBlock.getFreeMemoryInBytes();
   }
+
+  public long getOvercommitSizeInBytes() {
+    return Math.max(0L, getUsedMemorySizeInBytes() - 
getTotalMemorySizeInBytes());
+  }
+
+  public long getOversizedEntryCount() {
+    return oversizedEntryCount;
+  }
 }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueDataNodeMemoryTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueDataNodeMemoryTest.java
index 60f30834cad..f9c56e7b56f 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueDataNodeMemoryTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueDataNodeMemoryTest.java
@@ -33,6 +33,7 @@ import 
org.apache.iotdb.db.subscription.event.SubscriptionEvent;
 import org.apache.iotdb.db.subscription.resource.SubscriptionMemoryManager;
 import org.apache.iotdb.rpc.subscription.config.TopicConstant;
 import org.apache.iotdb.rpc.subscription.payload.poll.RegionProgress;
+import 
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionPollResponseType;
 
 import org.apache.tsfile.enums.ColumnCategory;
 import org.apache.tsfile.enums.TSDataType;
@@ -45,12 +46,19 @@ import org.junit.rules.TemporaryFolder;
 import java.io.File;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
+import java.lang.reflect.Method;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -67,6 +75,15 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
 
   @Test
   public void testQueuesShareDataNodeMemoryBudget() throws Exception {
+    assertQueuesShareDataNodeMemoryBudget(false);
+  }
+
+  @Test
+  public void testOversizedEntryBlocksOtherQueueUntilAck() throws Exception {
+    assertQueuesShareDataNodeMemoryBudget(true);
+  }
+
+  private void assertQueuesShareDataNodeMemoryBudget(final boolean oversized) 
throws Exception {
     final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
     final int originalBatchMaxDelay =
         
CommonDescriptor.getInstance().getConfig().getSubscriptionConsensusBatchMaxDelayInMs();
@@ -77,7 +94,8 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
       
CommonDescriptor.getInstance().getConfig().setSubscriptionConsensusBatchMaxDelayInMs(0);
       final Tablet tablet = createTablet();
       final long oneTabletBytes = 
PipeMemoryWeightUtil.calculateTabletSizeInBytes(tablet);
-      final SubscriptionMemoryManager memoryManager = new 
SubscriptionMemoryManager(oneTabletBytes);
+      final SubscriptionMemoryManager memoryManager =
+          new SubscriptionMemoryManager(oneTabletBytes - (oversized ? 1L : 
0L));
       final ConsensusSubscriptionCommitManager commitManager = 
newCommitManager(systemDir);
 
       final FakeConsensusReqReader readerA = new FakeConsensusReqReader();
@@ -116,9 +134,10 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
       assertEquals(oneTabletBytes, queueA.getRetainedTabletBytes());
       assertEquals(0L, queueB.getRetainedTabletBytes());
       assertEquals(oneTabletBytes, memoryManager.getUsedMemorySizeInBytes());
-      assertTrue(
-          queueA.getRetainedTabletBytes() + queueB.getRetainedTabletBytes()
-              <= memoryManager.getTotalMemorySizeInBytes());
+      assertEquals(oversized ? 1L : 0L, 
memoryManager.getOvercommitSizeInBytes());
+      assertEquals(oversized ? 1L : 0L, 
memoryManager.getOversizedEntryCount());
+      assertEquals(
+          oneTabletBytes, queueA.getRetainedTabletBytes() + 
queueB.getRetainedTabletBytes());
 
       readerB.currentSearchIndex = 1L;
       assertFalse(pendingEntries(queueB).offer(createRequest(1L)));
@@ -132,6 +151,7 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
       assertTrue(queueA.ack("consumerA", eventA.getCommitContext()));
       assertEquals(0L, queueA.getRetainedTabletBytes());
       assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
+      assertEquals(0L, memoryManager.getOvercommitSizeInBytes());
 
       queueB.drivePrefetchOnce();
       assertEquals("false", 
queueB.coreReportMessage().get("realtimeAdmissionBlocked"));
@@ -141,14 +161,16 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
       assertEquals(1, conversionCountB.get());
       assertEquals(oneTabletBytes, queueB.getRetainedTabletBytes());
       assertEquals(oneTabletBytes, memoryManager.getUsedMemorySizeInBytes());
-      assertTrue(
-          queueA.getRetainedTabletBytes() + queueB.getRetainedTabletBytes()
-              <= memoryManager.getTotalMemorySizeInBytes());
+      assertEquals(oversized ? 1L : 0L, 
memoryManager.getOvercommitSizeInBytes());
+      assertEquals(oversized ? 2L : 0L, 
memoryManager.getOversizedEntryCount());
+      assertEquals(
+          oneTabletBytes, queueA.getRetainedTabletBytes() + 
queueB.getRetainedTabletBytes());
 
       final SubscriptionEvent eventB = queueB.poll("consumerB");
       assertNotNull(eventB);
       assertTrue(queueB.ack("consumerB", eventB.getCommitContext()));
       assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
+      assertEquals(0L, memoryManager.getOvercommitSizeInBytes());
     } finally {
       if (queueA != null) {
         queueA.close();
@@ -163,6 +185,150 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
     }
   }
 
+  @Test
+  public void testWatermarkAndAckDoNotHideDataDeliveryGap() throws Exception {
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final int originalBatchMaxDelay =
+        
CommonDescriptor.getInstance().getConfig().getSubscriptionConsensusBatchMaxDelayInMs();
+    final File systemDir = temporaryFolder.newFolder("delivery-gap");
+    ConsensusPrefetchingQueue queue = null;
+    try {
+      
CommonDescriptor.getInstance().getConfig().setSubscriptionConsensusBatchMaxDelayInMs(0);
+      final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+      queue =
+          newQueue(
+              "consumerGroup",
+              new DataRegionId(1),
+              reader,
+              newConverter(new AtomicInteger()),
+              newCommitManager(systemDir),
+              TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE);
+      final AtomicLong clockNs = new AtomicLong(-1_000_000L);
+      setTimeTracker(queue, new SubscriptionQueueTimeTracker(clockNs::get));
+      queue.setSubscriptionMemoryManager(new 
SubscriptionMemoryManager(1_000_000L));
+
+      assertNull(queue.poll("consumer"));
+      reader.currentSearchIndex = 1L;
+      assertTrue(pendingEntries(queue).offer(createRequest(1L)));
+      queue.drivePrefetchOnce();
+      final SubscriptionEvent first = queue.poll("consumer");
+      assertNotNull(first);
+
+      clockNs.addAndGet(TimeUnit.MILLISECONDS.toNanos(60_000L));
+      final Method injectWatermark =
+          ConsensusPrefetchingQueue.class.getDeclaredMethod("injectWatermark", 
long.class);
+      injectWatermark.setAccessible(true);
+      injectWatermark.invoke(queue, 1L);
+      final SubscriptionEvent watermark = queue.poll("consumer");
+      assertNotNull(watermark);
+      assertEquals(
+          SubscriptionPollResponseType.WATERMARK.getType(),
+          watermark.getCurrentResponse().getResponseType());
+      assertEquals(60_000L, queue.getDeliveryIdleTimeMs());
+      assertTrue(queue.ack("consumer", first.getCommitContext()));
+      assertEquals(60_000L, queue.getDeliveryIdleTimeMs());
+      assertEquals(0L, queue.getLastDeliveryIntervalMs());
+
+      clockNs.addAndGet(TimeUnit.MILLISECONDS.toNanos(1L));
+      reader.currentSearchIndex = 2L;
+      assertTrue(pendingEntries(queue).offer(createRequest(2L)));
+      queue.drivePrefetchOnce();
+      final SubscriptionEvent second = queue.poll("consumer");
+      assertNotNull(second);
+      assertEquals(60_001L, queue.getLastDeliveryIntervalMs());
+      assertEquals(60_001L, queue.getMaxDeliveryIntervalMs());
+      assertEquals(0L, queue.getDeliveryIdleTimeMs());
+      assertTrue(queue.ack("consumer", second.getCommitContext()));
+
+      clockNs.addAndGet(TimeUnit.MILLISECONDS.toNanos(1L));
+      reader.currentSearchIndex = 3L;
+      assertTrue(pendingEntries(queue).offer(createRequest(3L)));
+      queue.drivePrefetchOnce();
+      assertNotNull(queue.poll("consumer"));
+      assertEquals(1L, queue.getLastDeliveryIntervalMs());
+      assertEquals(60_001L, queue.getMaxDeliveryIntervalMs());
+    } finally {
+      if (queue != null) {
+        queue.close();
+      }
+      CommonDescriptor.getInstance()
+          .getConfig()
+          .setSubscriptionConsensusBatchMaxDelayInMs(originalBatchMaxDelay);
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
+  @Test
+  public void testTimingRemainsReadableDuringBlockedPrefetch() throws 
Exception {
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final File systemDir = 
temporaryFolder.newFolder("blocked-prefetch-timing");
+    final CountDownLatch conversionStarted = new CountDownLatch(1);
+    final CountDownLatch releaseConversion = new CountDownLatch(1);
+    final ExecutorService executor = Executors.newSingleThreadExecutor();
+    ConsensusPrefetchingQueue queue = null;
+    try {
+      final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+      final ConsensusLogToTabletConverter converter = 
mock(ConsensusLogToTabletConverter.class);
+      when(converter.getDatabaseName()).thenReturn("db");
+      when(converter.convert(any()))
+          .thenAnswer(
+              ignored -> {
+                conversionStarted.countDown();
+                assertTrue(releaseConversion.await(5L, TimeUnit.SECONDS));
+                return Collections.singletonList(createTablet());
+              });
+      queue =
+          newQueue(
+              "consumerGroup",
+              new DataRegionId(1),
+              reader,
+              converter,
+              newCommitManager(systemDir),
+              TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE);
+      final AtomicLong clockNs = new AtomicLong(Long.MAX_VALUE - 1_000_000L);
+      setTimeTracker(queue, new SubscriptionQueueTimeTracker(clockNs::get));
+      queue.setSubscriptionMemoryManager(new 
SubscriptionMemoryManager(1_000_000L));
+
+      assertNull(queue.poll("consumer"));
+      reader.currentSearchIndex = 1L;
+      assertTrue(pendingEntries(queue).offer(createRequest(1L)));
+      final ConsensusPrefetchingQueue blockedQueue = queue;
+      final Future<PrefetchRoundResult> round = 
executor.submit(blockedQueue::drivePrefetchOnce);
+      assertTrue(conversionStarted.await(5L, TimeUnit.SECONDS));
+      clockNs.addAndGet(TimeUnit.MILLISECONDS.toNanos(65_000L));
+
+      assertEquals(65_000L, queue.getPrefetchDurationMs());
+      assertEquals(65_000L, queue.getMaxPrefetchDurationMs());
+      assertEquals(65_000L, queue.getPrefetchIdleTimeMs());
+      assertEquals(65_000L, queue.getDeliveryIdleTimeMs());
+      assertEquals("65000", 
queue.coreReportMessage().get("prefetchDurationMs"));
+      assertEquals("0", 
queue.coreReportMessage().get("subscriptionMemoryUsedInBytes"));
+
+      releaseConversion.countDown();
+      round.get(5L, TimeUnit.SECONDS);
+      assertEquals(0L, queue.getPrefetchDurationMs());
+      assertEquals(65_000L, queue.getMaxPrefetchDurationMs());
+      assertEquals(0L, queue.getPrefetchIdleTimeMs());
+      queue.drivePrefetchOnce();
+      assertEquals(65_000L, queue.getMaxPrefetchDurationMs());
+    } finally {
+      releaseConversion.countDown();
+      executor.shutdownNow();
+      if (queue != null) {
+        queue.close();
+      }
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
+  private static void setTimeTracker(
+      final ConsensusPrefetchingQueue queue, final 
SubscriptionQueueTimeTracker tracker)
+      throws Exception {
+    final Field field = 
ConsensusPrefetchingQueue.class.getDeclaredField("timeTracker");
+    field.setAccessible(true);
+    field.set(queue, tracker);
+  }
+
   @Test
   public void 
testRealtimeBacklogIsDrainedBeforeMorePendingEntriesAreConverted() throws 
Exception {
     final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetricsRestartTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetricsRestartTest.java
index a2142ad61fd..68eee16ee2f 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetricsRestartTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/metric/SubscriptionMetricsRestartTest.java
@@ -24,6 +24,8 @@ import org.apache.iotdb.commons.service.metric.MetricService;
 import org.apache.iotdb.commons.service.metric.enums.Metric;
 import org.apache.iotdb.db.subscription.broker.SubscriptionPrefetchingQueue;
 import 
org.apache.iotdb.db.subscription.broker.consensus.ConsensusPrefetchingQueue;
+import 
org.apache.iotdb.db.subscription.resource.SubscriptionDataNodeResourceManager;
+import org.apache.iotdb.db.subscription.resource.SubscriptionMemoryManager;
 import org.apache.iotdb.metrics.config.MetricConfig;
 import org.apache.iotdb.metrics.config.MetricConfigDescriptor;
 import org.apache.iotdb.metrics.metricsets.IMetricSet;
@@ -38,12 +40,15 @@ import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mockito;
 
+import java.lang.reflect.Field;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.when;
 
 /** Subscription metrics must survive a metric service restart, which drops 
all metrics. */
@@ -96,6 +101,52 @@ public class SubscriptionMetricsRestartTest {
     assertEquals(0, count(Metric.SUBSCRIPTION_EVENT_TRANSFER));
   }
 
+  @Test
+  public void testSubscriptionMemoryMetrics() throws Exception {
+    final SubscriptionMetrics metrics = SubscriptionMetrics.getInstance();
+    // Subscription can be disabled in the default test configuration. Give 
the shared allocator
+    // an explicit test budget and restore its original block after removing 
the metrics.
+    final long budget = 10L;
+    final SubscriptionMemoryManager memoryManager = 
SubscriptionDataNodeResourceManager.memory();
+    final long originalOversizedEntryCount = 
memoryManager.getOversizedEntryCount();
+    final Field blockField = 
SubscriptionMemoryManager.class.getDeclaredField("memoryBlock");
+    blockField.setAccessible(true);
+    final Object originalBlock = blockField.get(memoryManager);
+    try {
+      blockField.set(memoryManager, blockField.get(new 
SubscriptionMemoryManager(budget)));
+      bind(metrics);
+      assertTrue(memoryManager.tryAllocate(budget + 1L));
+      try {
+        service.restartService();
+        assertEquals(
+            budget, ((AutoGauge) 
get(Metric.SUBSCRIPTION_MEMORY_LIMIT_BYTES)).getValue(), 0);
+        assertEquals(
+            budget + 1L, ((AutoGauge) 
get(Metric.SUBSCRIPTION_MEMORY_USED_BYTES)).getValue(), 0);
+        assertEquals(
+            1, ((AutoGauge) 
get(Metric.SUBSCRIPTION_MEMORY_OVERCOMMIT_BYTES)).getValue(), 0);
+        assertEquals(
+            originalOversizedEntryCount + 1L,
+            ((AutoGauge) 
get(Metric.SUBSCRIPTION_MEMORY_OVERSIZED_ENTRY_COUNT)).getValue(),
+            0);
+        assertFalse(memoryManager.tryAllocate(1L));
+      } finally {
+        memoryManager.release(budget + 1L);
+      }
+      assertEquals(0, ((AutoGauge) 
get(Metric.SUBSCRIPTION_MEMORY_USED_BYTES)).getValue(), 0);
+      assertEquals(0, ((AutoGauge) 
get(Metric.SUBSCRIPTION_MEMORY_OVERCOMMIT_BYTES)).getValue(), 0);
+      service.removeMetricSet(metrics);
+      boundMetricSets.remove(metrics);
+      assertEquals(0, count(Metric.SUBSCRIPTION_MEMORY_USED_BYTES));
+      assertEquals(0, count(Metric.SUBSCRIPTION_MEMORY_LIMIT_BYTES));
+      assertEquals(0, count(Metric.SUBSCRIPTION_MEMORY_OVERCOMMIT_BYTES));
+      assertEquals(0, count(Metric.SUBSCRIPTION_MEMORY_OVERSIZED_ENTRY_COUNT));
+    } finally {
+      service.removeMetricSet(metrics);
+      boundMetricSets.remove(metrics);
+      blockField.set(memoryManager, originalBlock);
+    }
+  }
+
   @Test
   public void testConsensusPrefetchingQueueMetrics() {
     final ConsensusSubscriptionPrefetchingQueueMetrics metrics =
@@ -105,16 +156,52 @@ public class SubscriptionMetricsRestartTest {
     when(queue.getPrefetchingQueueId()).thenReturn(QUEUE_ID);
     when(queue.getConsensusGroupId()).thenReturn(new DataRegionId(1));
     when(queue.getLag()).thenReturn(7L);
+    when(queue.getLastDeliveryIntervalMs()).thenReturn(60_001L);
+    when(queue.getMaxDeliveryIntervalMs()).thenReturn(65_000L);
+    when(queue.getDeliveryIdleTimeMs()).thenReturn(60_000L);
+    when(queue.getPrefetchDurationMs()).thenReturn(65_000L);
+    when(queue.getMaxPrefetchDurationMs()).thenReturn(69_000L);
+    when(queue.getPrefetchIdleTimeMs()).thenReturn(67_000L);
     metrics.register(queue);
     try {
       service.restartService();
       assertEquals(7, ((AutoGauge) 
get(Metric.SUBSCRIPTION_CONSENSUS_LAG)).getValue(), 0);
+      assertEquals(
+          60_001,
+          ((AutoGauge) 
get(Metric.SUBSCRIPTION_CONSENSUS_LAST_DELIVERY_INTERVAL_MS)).getValue(),
+          0);
+      assertEquals(
+          65_000,
+          ((AutoGauge) 
get(Metric.SUBSCRIPTION_CONSENSUS_MAX_DELIVERY_INTERVAL_MS)).getValue(),
+          0);
+      assertEquals(
+          60_000,
+          ((AutoGauge) 
get(Metric.SUBSCRIPTION_CONSENSUS_DELIVERY_IDLE_TIME_MS)).getValue(),
+          0);
+      assertEquals(
+          65_000,
+          ((AutoGauge) 
get(Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_DURATION_MS)).getValue(),
+          0);
+      assertEquals(
+          69_000,
+          ((AutoGauge) 
get(Metric.SUBSCRIPTION_CONSENSUS_MAX_PREFETCH_DURATION_MS)).getValue(),
+          0);
+      assertEquals(
+          67_000,
+          ((AutoGauge) 
get(Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_IDLE_TIME_MS)).getValue(),
+          0);
       metrics.mark(QUEUE_ID, new DataRegionId(1).toString(), 5);
       assertEquals(5, ((Rate) 
get(Metric.SUBSCRIPTION_EVENT_TRANSFER)).getCount());
     } finally {
       metrics.deregister(queue);
     }
     assertEquals(0, count(Metric.SUBSCRIPTION_CONSENSUS_LAG));
+    assertEquals(0, 
count(Metric.SUBSCRIPTION_CONSENSUS_LAST_DELIVERY_INTERVAL_MS));
+    assertEquals(0, 
count(Metric.SUBSCRIPTION_CONSENSUS_MAX_DELIVERY_INTERVAL_MS));
+    assertEquals(0, 
count(Metric.SUBSCRIPTION_CONSENSUS_DELIVERY_IDLE_TIME_MS));
+    assertEquals(0, count(Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_DURATION_MS));
+    assertEquals(0, 
count(Metric.SUBSCRIPTION_CONSENSUS_MAX_PREFETCH_DURATION_MS));
+    assertEquals(0, 
count(Metric.SUBSCRIPTION_CONSENSUS_PREFETCH_IDLE_TIME_MS));
     assertEquals(0, count(Metric.SUBSCRIPTION_EVENT_TRANSFER));
   }
 
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManagerTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManagerTest.java
index 1635a6e0ecf..5b77b5d1406 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManagerTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/resource/SubscriptionMemoryManagerTest.java
@@ -35,6 +35,8 @@ public class SubscriptionMemoryManagerTest {
     assertFalse(memoryManager.tryAllocate(5L));
     assertEquals(6L, memoryManager.getUsedMemorySizeInBytes());
     assertEquals(4L, memoryManager.getFreeMemorySizeInBytes());
+    assertEquals(0L, memoryManager.getOvercommitSizeInBytes());
+    assertEquals(0L, memoryManager.getOversizedEntryCount());
 
     memoryManager.release(6L);
     assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
@@ -48,10 +50,19 @@ public class SubscriptionMemoryManagerTest {
     assertTrue(memoryManager.tryAllocate(11L));
     assertFalse(memoryManager.tryAllocate(1L));
     assertEquals(11L, memoryManager.getUsedMemorySizeInBytes());
+    assertEquals(1L, memoryManager.getOvercommitSizeInBytes());
+    assertEquals(1L, memoryManager.getOversizedEntryCount());
 
     memoryManager.release(11L);
     assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
+    assertEquals(0L, memoryManager.getOvercommitSizeInBytes());
     assertTrue(memoryManager.tryAllocate(10L));
+    assertFalse(memoryManager.tryAllocate(11L));
+    assertEquals(1L, memoryManager.getOversizedEntryCount());
+    memoryManager.release(10L);
+    assertTrue(memoryManager.tryAllocate(12L));
+    assertEquals(2L, memoryManager.getOvercommitSizeInBytes());
+    assertEquals(2L, memoryManager.getOversizedEntryCount());
   }
 
   @Test
@@ -60,5 +71,6 @@ public class SubscriptionMemoryManagerTest {
 
     assertFalse(memoryManager.tryAllocate(1L));
     assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
+    assertEquals(0L, memoryManager.getOversizedEntryCount());
   }
 }
diff --git 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/service/metric/enums/Metric.java
 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/service/metric/enums/Metric.java
index 5bcafffaa14..5d2a3c69b7b 100644
--- 
a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/service/metric/enums/Metric.java
+++ 
b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/service/metric/enums/Metric.java
@@ -231,7 +231,20 @@ public enum Metric {
   
SUBSCRIPTION_CONSENSUS_PENDING_EVENT_COUNT("subscription_consensus_pending_event_count"),
   
SUBSCRIPTION_CONSENSUS_LAST_POLL_TIME("subscription_consensus_last_poll_time"),
   
SUBSCRIPTION_CONSENSUS_LAST_PROGRESS_TIME("subscription_consensus_last_progress_time"),
+  SUBSCRIPTION_CONSENSUS_LAST_DELIVERY_INTERVAL_MS(
+      "subscription_consensus_last_delivery_interval_ms"),
+  SUBSCRIPTION_CONSENSUS_MAX_DELIVERY_INTERVAL_MS(
+      "subscription_consensus_max_delivery_interval_ms"),
+  
SUBSCRIPTION_CONSENSUS_DELIVERY_IDLE_TIME_MS("subscription_consensus_delivery_idle_time_ms"),
+  
SUBSCRIPTION_CONSENSUS_PREFETCH_DURATION_MS("subscription_consensus_prefetch_duration_ms"),
+  SUBSCRIPTION_CONSENSUS_MAX_PREFETCH_DURATION_MS(
+      "subscription_consensus_max_prefetch_duration_ms"),
+  
SUBSCRIPTION_CONSENSUS_PREFETCH_IDLE_TIME_MS("subscription_consensus_prefetch_idle_time_ms"),
   
SUBSCRIPTION_CONSENSUS_PROGRESS_STATUS("subscription_consensus_progress_status"),
+  SUBSCRIPTION_MEMORY_USED_BYTES("subscription_memory_used_bytes"),
+  SUBSCRIPTION_MEMORY_LIMIT_BYTES("subscription_memory_limit_bytes"),
+  SUBSCRIPTION_MEMORY_OVERCOMMIT_BYTES("subscription_memory_overcommit_bytes"),
+  
SUBSCRIPTION_MEMORY_OVERSIZED_ENTRY_COUNT("subscription_memory_oversized_entry_count"),
   // load related
   ACTIVE_LOADING_FILES_NUMBER("active_loading_files_number"),
   ACTIVE_LOADING_FILES_SIZE("active_loading_files_size"),

Reply via email to