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

jt2594838 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 3f1706cbce0 Fix subscription realtime retention under shared memory 
pressure (#18870)
3f1706cbce0 is described below

commit 3f1706cbce05445ddd84e4867097f3c575bd02c8
Author: Caideyipi <[email protected]>
AuthorDate: Thu Oct 15 10:35:43 2026 +0800

    Fix subscription realtime retention under shared memory pressure (#18870)
---
 .../consensus/ConsensusPrefetchingQueue.java       | 131 ++++++++++------
 ...onsensusPrefetchingQueueDataNodeMemoryTest.java | 170 +++++++++++++++++++--
 .../consensus/ConsensusPrefetchingQueueTest.java   |  93 ++++++-----
 ...nsensusPrefetchingQueueWalBackpressureTest.java |  20 ++-
 4 files changed, 309 insertions(+), 105 deletions(-)

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 c19420c130c..1dd733b4cae 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
@@ -205,7 +205,7 @@ public class ConsensusPrefetchingQueue {
    */
   private volatile long memoryBlockedEntryBytes = -1L;
 
-  /** Rejects the real-time fast path while this queue cannot safely 
materialize more data. */
+  /** Rejects the real-time fast path during lifecycle transitions or a 
blocked writer backlog. */
   private final AtomicBoolean realtimeAdmissionBlocked = new 
AtomicBoolean(false);
 
   private static final int MAX_PREFETCHING_QUEUE_SIZE =
@@ -534,8 +534,10 @@ public class ConsensusPrefetchingQueue {
       requestMemoryManager.free(request);
     }
 
-    private void release(final List<IndexedConsensusRequest> requests) {
-      requests.forEach(this::release);
+    private synchronized void consume(final IndexedConsensusRequest request) {
+      if (super.removeFirstOccurrence(request)) {
+        release(request);
+      }
     }
 
     private long getRetainedRequestBytes() {
@@ -751,11 +753,29 @@ public class ConsensusPrefetchingQueue {
   }
 
   private void requestPrefetchForRealtimeEntry() {
-    if (prefetchingQueue.size() < MAX_PREFETCHING_QUEUE_SIZE) {
+    if (!hasReachedPrefetchLimit()) {
       requestPrefetch();
     }
   }
 
+  private boolean hasReachedPrefetchLimit() {
+    // Reserve half of the active queue's fair share for speculative 
read-ahead. Retained bytes
+    // include in-flight events, so polling without ACK cannot bypass the 
memory target. Allow an
+    // empty delivery queue to make progress even when a single entry exceeds 
the target; the
+    // shared memory manager still enforces the hard allocation limit.
+    return prefetchingQueue.size() >= MAX_PREFETCHING_QUEUE_SIZE
+        || (!prefetchingQueue.isEmpty()
+            && retainedTabletBytes.get()
+                >= Math.max(1L, 
subscriptionMemoryHandle.getMemoryQuotaInBytes() / 2L));
+  }
+
+  private boolean hasReachedPrefetchLimit(final DeliveryBatchState batchState) 
{
+    // The current batch becomes one more event when flushed. Stop conversion 
before that event
+    // fills the delivery queue so the unprocessed pending suffix stays queued.
+    return hasReachedPrefetchLimit()
+        || (!batchState.isEmpty() && prefetchingQueue.size() >= 
MAX_PREFETCHING_QUEUE_SIZE - 1);
+  }
+
   private SubscriptionQueueRejectionReason 
getRealtimeAdmissionRejectionReason() {
     if (!isActive || closeRequested || isClosed) {
       return SubscriptionQueueRejectionReason.INACTIVE_OR_CLOSED;
@@ -763,13 +783,8 @@ public class ConsensusPrefetchingQueue {
     if (realtimeAdmissionBlocked.get()) {
       return realtimeAdmissionBlockReason;
     }
-    // Allow an empty queue to probe an entry even with a zero quota so 
materialization can report
-    // an explicit oversized-entry error. Pending requests have separate 
consensus memory
-    // accounting.
-    if (subscriptionMemoryHandle.getUsedMemorySizeInBytes() > 0L
-        && subscriptionMemoryHandle.getFreeMemorySizeInBytes() <= 0L) {
-      return 
toQueueRejectionReason(subscriptionMemoryHandle.inspectRejection(1L));
-    }
+    // Pending requests have their own bounded queue and consensus memory 
accounting. Subscription
+    // memory pressure must stop materialization without forcing admitted 
realtime data into WAL.
     return SubscriptionQueueRejectionReason.NONE;
   }
 
@@ -917,7 +932,7 @@ public class ConsensusPrefetchingQueue {
         return null;
       }
       final SubscriptionEvent event = pollInternal(consumerId);
-      if (Objects.nonNull(event) && prefetchingQueue.size() < 
MAX_PREFETCHING_QUEUE_SIZE) {
+      if (Objects.nonNull(event) && !hasReachedPrefetchLimit()) {
         requestPrefetch();
       } else if (Objects.isNull(event) && 
shouldRecoverPrefetchBindingAfterEmptyPoll()) {
         requestPrefetch();
@@ -1704,24 +1719,22 @@ public class ConsensusPrefetchingQueue {
         resetRoundStateForSeek(seekGeneration.get());
         return PrefetchRoundResult.rescheduleNow();
       }
-      if (prefetchingQueue.size() >= MAX_PREFETCHING_QUEUE_SIZE
-          || !realtimeEntriesByWriter.isEmpty()) {
-        if (!realtimeEntriesByWriter.isEmpty()) {
-          
blockRealtimeAdmission(SubscriptionQueueRejectionReason.WRITER_BACKLOG);
-        }
+      if (!realtimeEntriesByWriter.isEmpty()) {
+        
blockRealtimeAdmission(SubscriptionQueueRejectionReason.WRITER_BACKLOG);
+        return computeIdleRoundResult(walScanBudget);
+      }
+      unblockRealtimeAdmission();
+      if (hasReachedPrefetchLimit()) {
         return computeIdleRoundResult(walScanBudget);
       }
       if (shouldWaitForSubscriptionMemory()) {
-        blockRealtimeAdmission(memoryBlockReason);
         if (!lingerBatch.isEmpty() && !flushBatch(lingerBatch, 
observedSeekGeneration)) {
           resetRoundStateForSeek(seekGeneration.get());
           return PrefetchRoundResult.rescheduleNow();
         }
         return PrefetchRoundResult.rescheduleAfter(MEMORY_RETRY_SLEEP_MS);
       }
-      unblockRealtimeAdmission();
-
-      final List<IndexedConsensusRequest> batch = 
drainPendingEntries(maxWalEntries);
+      final List<IndexedConsensusRequest> batch = 
snapshotPendingEntries(maxWalEntries);
       if (!batch.isEmpty()) {
         LOGGER.debug(
             DataNodePipeMessages
@@ -1733,20 +1746,15 @@ public class ConsensusPrefetchingQueue {
             nextExpectedSearchIndex.get(),
             prefetchingQueue.size());
 
-        final MaterializationResult batchResult;
-        try {
-          batchResult =
-              accumulateFromPending(
-                  batch,
-                  lingerBatch,
-                  observedSeekGeneration,
-                  maxWalEntries,
-                  maxTablets,
-                  maxBatchBytes,
-                  walScanBudget);
-        } finally {
-          pendingEntries.release(batch);
-        }
+        final MaterializationResult batchResult =
+            accumulateFromPending(
+                batch,
+                lingerBatch,
+                observedSeekGeneration,
+                maxWalEntries,
+                maxTablets,
+                maxBatchBytes,
+                walScanBudget);
         if (batchResult != MaterializationResult.SUCCESS) {
           if (batchResult == MaterializationResult.WAL_SCAN_YIELDED) {
             if (!lingerBatch.isEmpty() && !flushBatch(lingerBatch, 
observedSeekGeneration)) {
@@ -1763,8 +1771,7 @@ public class ConsensusPrefetchingQueue {
           }
           if (batchResult == MaterializationResult.MEMORY_BLOCKED) {
             // Publish already reserved Tablets immediately so ACK can release 
their memory. The
-            // blocked request and the drained suffix are recovered from WAL 
on a later round.
-            blockRealtimeAdmission(memoryBlockReason);
+            // blocked request and the unprocessed suffix remain in the 
bounded pending queue.
             if (!lingerBatch.isEmpty() && !flushBatch(lingerBatch, 
observedSeekGeneration)) {
               resetRoundStateForSeek(seekGeneration.get());
               return PrefetchRoundResult.rescheduleNow();
@@ -1783,7 +1790,6 @@ public class ConsensusPrefetchingQueue {
           return PrefetchRoundResult.rescheduleNow();
         }
         if (walResult == MaterializationResult.MEMORY_BLOCKED) {
-          blockRealtimeAdmission(memoryBlockReason);
           return PrefetchRoundResult.rescheduleAfter(MEMORY_RETRY_SLEEP_MS);
         }
         if (walResult == MaterializationResult.STALE) {
@@ -1798,6 +1804,15 @@ public class ConsensusPrefetchingQueue {
         return PrefetchRoundResult.rescheduleNow();
       }
 
+      // Publish reserved Tablets when read-ahead pauses. Leaving this batch 
lingering at the
+      // delivery limit would keep rescheduling pending work that cannot yet 
be materialized.
+      if (!lingerBatch.isEmpty() && hasReachedPrefetchLimit(lingerBatch)) {
+        if (!flushBatch(lingerBatch, observedSeekGeneration)) {
+          resetRoundStateForSeek(seekGeneration.get());
+          return PrefetchRoundResult.rescheduleNow();
+        }
+      }
+
       if (!lingerBatch.isEmpty() && lingerBatch.firstTabletTimeMs > 0L) {
         final long lingerElapsedMs = System.currentTimeMillis() - 
lingerBatch.firstTabletTimeMs;
         if (lingerElapsedMs >= batchMaxDelayMs) {
@@ -1877,11 +1892,15 @@ public class ConsensusPrefetchingQueue {
     observedSeekGeneration = newSeekGeneration;
   }
 
-  private List<IndexedConsensusRequest> drainPendingEntries(final int 
maxWalEntries) {
+  private List<IndexedConsensusRequest> snapshotPendingEntries(final int 
maxWalEntries) {
+    // The queue read lock excludes lifecycle clears. Concurrent producers may 
only append, so
+    // retain each request and its consensus memory reservation until it is 
actually consumed.
     final List<IndexedConsensusRequest> batch = new ArrayList<>();
-    IndexedConsensusRequest next;
-    while (batch.size() < maxWalEntries && Objects.nonNull(next = 
pendingEntries.poll())) {
-      batch.add(next);
+    for (final IndexedConsensusRequest request : pendingEntries) {
+      if (batch.size() >= maxWalEntries) {
+        break;
+      }
+      batch.add(request);
     }
     return batch;
   }
@@ -1897,7 +1916,7 @@ public class ConsensusPrefetchingQueue {
     if (isClosed || !prefetchInitialized || !isActive) {
       return PrefetchRoundResult.dormant();
     }
-    if (prefetchingQueue.size() >= MAX_PREFETCHING_QUEUE_SIZE) {
+    if (hasReachedPrefetchLimit()) {
       return PrefetchRoundResult.dormant();
     }
     if (hasImmediatePrefetchableWork(walScanBudget)) {
@@ -2024,7 +2043,10 @@ public class ConsensusPrefetchingQueue {
    * Accumulates tablets from pending entries into the linger buffer. When 
pending replay outruns
    * the local WAL reader, this method backfills the local-index gap from WAL 
before continuing.
    *
-   * @return the materialization result for the drained pending batch
+   * <p>Requests are removed only after materialization or deduplication. 
Memory pressure, delivery
+   * capacity, and temporary WAL gaps leave the unprocessed suffix available 
for realtime delivery.
+   *
+   * @return the materialization result for the pending batch
    */
   private MaterializationResult accumulateFromPending(
       final List<IndexedConsensusRequest> batch,
@@ -2041,6 +2063,9 @@ public class ConsensusPrefetchingQueue {
 
     try {
       for (int index = 0; index < batch.size(); index++) {
+        if (hasReachedPrefetchLimit(lingerBatch)) {
+          break;
+        }
         final IndexedConsensusRequest request = batch.get(index);
         final long searchIndex = request.getSearchIndex();
 
@@ -2067,21 +2092,27 @@ public class ConsensusPrefetchingQueue {
           if (gapFillResult != MaterializationResult.SUCCESS) {
             return gapFillResult;
           }
+          if (hasReachedPrefetchLimit(lingerBatch)) {
+            break;
+          }
         }
 
         if (isBeforeLocalCursor(request)) {
           skippedCount++;
+          pendingEntries.consume(request);
           continue;
         }
 
         if (shouldSkipForRecoveryProgress(request)) {
           skippedCount++;
           advanceLocalCursorIfPresent(request);
+          pendingEntries.consume(request);
           continue;
         }
         if (shouldSkipForMaterializedProgress(request)) {
           skippedCount++;
           advanceLocalCursorIfPresent(request);
+          pendingEntries.consume(request);
           continue;
         }
 
@@ -2094,7 +2125,8 @@ public class ConsensusPrefetchingQueue {
         markMaterializedProgress(request);
         processedCount++;
         advanceLocalCursorIfPresent(request);
-        if (prefetchingQueue.size() >= MAX_PREFETCHING_QUEUE_SIZE) {
+        pendingEntries.consume(request);
+        if (hasReachedPrefetchLimit(lingerBatch)) {
           break;
         }
       }
@@ -2128,10 +2160,9 @@ public class ConsensusPrefetchingQueue {
    * Fills a gap in the pending queue by reading entries from WAL so the 
internal local replay
    * cursor stays contiguous even when pending delivery jumps ahead of the WAL 
iterator.
    *
-   * <p>Temporary WAL visibility lag is treated as a normal back-pressure 
condition: once a drained
-   * pending batch encounters an unresolved local-index gap, the queue backs 
off and lets the
-   * affected suffix fall back to the WAL path on later rounds. This keeps 
replay contiguous without
-   * requeueing the drained batch back into the bounded pending queue.
+   * <p>Temporary WAL visibility lag is treated as a normal back-pressure 
condition: an unresolved
+   * local-index gap pauses materialization while the affected suffix remains 
in the bounded pending
+   * queue. Later rounds backfill the gap before resuming pending delivery.
    *
    * @return the materialization result for the WAL gap
    */
@@ -2271,7 +2302,7 @@ public class ConsensusPrefetchingQueue {
 
     int entriesRead = 0;
     while (entriesRead < maxWalEntries
-        && prefetchingQueue.size() < MAX_PREFETCHING_QUEUE_SIZE
+        && !hasReachedPrefetchLimit(batchState)
         && subscriptionWALIterator.hasNext(walScanBudget)) {
       reportUnreadableWalReplayIfNecessary();
       try {
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 16f675a4da2..7a99e139733 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
@@ -19,6 +19,7 @@
 
 package org.apache.iotdb.db.subscription.broker.consensus;
 
+import org.apache.iotdb.commons.conf.CommonConfig;
 import org.apache.iotdb.commons.conf.CommonDescriptor;
 import org.apache.iotdb.commons.consensus.DataRegionId;
 import org.apache.iotdb.consensus.common.request.IndexedConsensusRequest;
@@ -50,6 +51,7 @@ import java.io.File;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
@@ -76,6 +78,149 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
 
   @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
 
+  @Test
+  public void testFullNodeMemoryDoesNotRejectRealtimeRequests() throws 
Exception {
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final CommonConfig config = CommonDescriptor.getInstance().getConfig();
+    final int originalBatchMaxDelay = 
config.getSubscriptionConsensusBatchMaxDelayInMs();
+    final List<ConsensusPrefetchingQueue> queues = new ArrayList<>();
+    final long oneTabletBytes = 
PipeMemoryWeightUtil.calculateTabletSizeInBytes(createTablet());
+    final SubscriptionMemoryManager memoryManager =
+        new SubscriptionMemoryManager(oneTabletBytes * 8L);
+    try {
+      config.setSubscriptionConsensusBatchMaxDelayInMs(0);
+      final ConsensusSubscriptionCommitManager commitManager =
+          
newCommitManager(temporaryFolder.newFolder("full-node-realtime-admission"));
+      for (int region = 1; region <= 4; region++) {
+        final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+        reader.currentSearchIndex = 2L;
+        final ConsensusPrefetchingQueue queue =
+            newQueue(
+                "consumerGroup",
+                new DataRegionId(region),
+                reader,
+                newConverter(new AtomicInteger()),
+                commitManager,
+                TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE);
+        queues.add(queue);
+        queue.setSubscriptionMemoryManager(memoryManager);
+        assertNull(queue.poll("consumer"));
+        assertTrue(pendingEntries(queue).offer(createRequest(1L)));
+        queue.drivePrefetchOnce();
+      }
+      // Exhaust the remaining budget through another subscription owner. 
Pending requests use
+      // consensus memory and must remain available for direct delivery after 
materialization
+      // resumes.
+      assertTrue(memoryManager.tryAllocate(oneTabletBytes * 4L));
+      assertEquals(0L, memoryManager.getFreeMemorySizeInBytes());
+      for (final ConsensusPrefetchingQueue queue : queues) {
+        assertTrue(pendingEntries(queue).offer(createRequest(2L)));
+        queue.drivePrefetchOnce();
+        assertEquals(1, pendingEntries(queue).size());
+        assertEquals("false", 
queue.coreReportMessage().get("realtimeAdmissionBlocked"));
+        final SubscriptionEvent event = queue.poll("consumer");
+        assertNotNull(event);
+        assertTrue(queue.ack("consumer", event.getCommitContext()));
+      }
+      memoryManager.release(oneTabletBytes * 4L);
+      for (final ConsensusPrefetchingQueue queue : queues) {
+        queue.drivePrefetchOnce();
+        final SubscriptionEvent event = queue.poll("consumer");
+        assertNotNull(event);
+        assertEquals(2L, 
event.getCommitContext().getWriterProgress().getLocalSeq());
+        assertTrue(queue.ack("consumer", event.getCommitContext()));
+        assertEquals(2L, queue.getPendingPathAcceptedEntries());
+        assertEquals(0L, queue.getWalPathAcceptedEntries());
+        assertEquals(0L, queue.getRealtimeAdmissionRejectionCount());
+        assertEquals(0L, queue.getRetainedRequestBytes());
+      }
+      assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
+    } finally {
+      queues.forEach(ConsensusPrefetchingQueue::close);
+      memoryManager.release(oneTabletBytes * 4L);
+      config.setSubscriptionConsensusBatchMaxDelayInMs(originalBatchMaxDelay);
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
+  @Test
+  public void testFourRegionsBoundReadAheadAndResumePendingDeliveryAfterAck() 
throws Exception {
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final CommonConfig config = CommonDescriptor.getInstance().getConfig();
+    final int originalBatchMaxDelay = 
config.getSubscriptionConsensusBatchMaxDelayInMs();
+    final int originalMaxTablets = 
config.getSubscriptionConsensusBatchMaxTabletCount();
+    final List<ConsensusPrefetchingQueue> queues = new ArrayList<>();
+    final List<AtomicInteger> conversionCounts = new ArrayList<>();
+    final long oneTabletBytes = 
PipeMemoryWeightUtil.calculateTabletSizeInBytes(createTablet());
+    final SubscriptionMemoryManager memoryManager =
+        new SubscriptionMemoryManager(oneTabletBytes * 16L);
+    final int requestCount = 12;
+    try {
+      config.setSubscriptionConsensusBatchMaxDelayInMs(0);
+      config.setSubscriptionConsensusBatchMaxTabletCount(1);
+      final ConsensusSubscriptionCommitManager commitManager =
+          
newCommitManager(temporaryFolder.newFolder("four-region-read-ahead"));
+      for (int region = 1; region <= 4; region++) {
+        final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+        reader.currentSearchIndex = requestCount;
+        final AtomicInteger conversionCount = new AtomicInteger();
+        conversionCounts.add(conversionCount);
+        final ConsensusPrefetchingQueue queue =
+            newQueue(
+                "consumerGroup",
+                new DataRegionId(region),
+                reader,
+                newConverter(conversionCount),
+                commitManager,
+                TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE);
+        queues.add(queue);
+        queue.setSubscriptionMemoryManager(memoryManager);
+        assertNull(queue.poll("consumer"));
+        for (long index = 1L; index <= requestCount; index++) {
+          assertTrue(pendingEntries(queue).offer(createRequest(index)));
+        }
+      }
+
+      for (int delivered = 0; delivered < requestCount; delivered += 2) {
+        for (int region = 0; region < queues.size(); region++) {
+          final ConsensusPrefetchingQueue queue = queues.get(region);
+          queue.drivePrefetchOnce();
+          assertEquals(2, queue.getPrefetchedEventCount());
+          assertEquals(requestCount - delivered - 2, 
pendingEntries(queue).size());
+          assertEquals(delivered + 2, conversionCounts.get(region).get());
+          // A paused consumer must stop further conversion while preserving 
the pending suffix.
+          queue.drivePrefetchOnce();
+          assertEquals(delivered + 2, conversionCounts.get(region).get());
+        }
+        assertTrue(
+            memoryManager.getUsedMemorySizeInBytes()
+                <= memoryManager.getTotalMemorySizeInBytes() / 2L);
+        assertEquals(0L, memoryManager.getOvercommitSizeInBytes());
+        for (final ConsensusPrefetchingQueue queue : queues) {
+          for (long index = delivered + 1L; index <= delivered + 2L; index++) {
+            final SubscriptionEvent event = queue.poll("consumer");
+            assertNotNull(event);
+            assertEquals(index, 
event.getCommitContext().getWriterProgress().getLocalSeq());
+            assertTrue(queue.ack("consumer", event.getCommitContext()));
+          }
+        }
+        assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
+      }
+      for (final ConsensusPrefetchingQueue queue : queues) {
+        assertEquals(requestCount, queue.getPendingPathAcceptedEntries());
+        assertEquals(0L, queue.getWalPathAcceptedEntries());
+        assertEquals(0L, queue.getWalGapSkippedEntries());
+        assertEquals(0L, queue.getRetainedRequestBytes());
+        assertEquals(0L, queue.getRealtimeAdmissionRejectionCount());
+      }
+    } finally {
+      queues.forEach(ConsensusPrefetchingQueue::close);
+      config.setSubscriptionConsensusBatchMaxDelayInMs(originalBatchMaxDelay);
+      config.setSubscriptionConsensusBatchMaxTabletCount(originalMaxTablets);
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
   @Test
   public void 
testLargeQueueQuotaDoesNotBlockSmallQueueAndPreservesPendingEntries()
       throws Exception {
@@ -133,15 +278,12 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
       assertEquals(1, largeConversions.get());
       assertEquals(1, pendingEntries(largeQueue).size());
       assertEquals(retainedPendingBytes, largeQueue.getRetainedRequestBytes());
-      assertFalse(pendingEntries(largeQueue).offer(createRequest(3L)));
+      assertTrue(pendingEntries(largeQueue).offer(createRequest(3L)));
       assertEquals(
-          SubscriptionQueueRejectionReason.SUBSCRIPTION_MEMORY_QUOTA,
+          SubscriptionQueueRejectionReason.NONE,
           ((SubscriptionQueueAdmission) 
pendingEntries(largeQueue)).getLastRejectionReason());
-      assertEquals(1L, largeQueue.getRealtimeAdmissionRejectionCount());
-      assertEquals(
-          1L,
-          largeQueue.getRealtimeAdmissionRejectionCount(
-              SubscriptionQueueRejectionReason.SUBSCRIPTION_MEMORY_QUOTA));
+      assertEquals(0L, largeQueue.getRealtimeAdmissionRejectionCount());
+      assertEquals(2, pendingEntries(largeQueue).size());
 
       smallReader.currentSearchIndex = 1L;
       assertTrue(pendingEntries(smallQueue).offer(createRequest(1L)));
@@ -158,11 +300,21 @@ public class ConsensusPrefetchingQueueDataNodeMemoryTest {
       assertTrue(largeQueue.ack("largeConsumer", 
largeEvent.getCommitContext()));
       largeQueue.drivePrefetchOnce();
       assertEquals(2, largeConversions.get());
-      assertEquals(0, pendingEntries(largeQueue).size());
-      assertEquals(0L, largeQueue.getRetainedRequestBytes());
+      assertEquals(1, pendingEntries(largeQueue).size());
+      assertTrue(largeQueue.getRetainedRequestBytes() > 0L);
       final SubscriptionEvent secondLargeEvent = 
largeQueue.poll("largeConsumer");
       assertNotNull(secondLargeEvent);
       assertTrue(largeQueue.ack("largeConsumer", 
secondLargeEvent.getCommitContext()));
+      largeQueue.drivePrefetchOnce();
+      final SubscriptionEvent thirdLargeEvent = 
largeQueue.poll("largeConsumer");
+      assertNotNull(thirdLargeEvent);
+      assertEquals(3L, 
thirdLargeEvent.getCommitContext().getWriterProgress().getLocalSeq());
+      assertTrue(largeQueue.ack("largeConsumer", 
thirdLargeEvent.getCommitContext()));
+      assertEquals(3, largeConversions.get());
+      assertEquals(0, pendingEntries(largeQueue).size());
+      assertEquals(0L, largeQueue.getRetainedRequestBytes());
+      assertEquals(3L, largeQueue.getPendingPathAcceptedEntries());
+      assertEquals(0L, largeQueue.getWalPathAcceptedEntries());
       assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
     } finally {
       if (largeQueue != null) {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
index c8bd2734e91..32f8e5737fd 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueTest.java
@@ -1808,11 +1808,18 @@ public class ConsensusPrefetchingQueueTest {
       queue.drivePrefetchOnce();
       assertEquals(2, conversionCount.get());
       assertEquals(3L, queue.getCurrentReadSearchIndex());
-      assertTrue(pendingEntries(queue).isEmpty());
+      assertEquals(1, pendingEntries(queue).size());
+      assertTrue(queue.getRetainedRequestBytes() > 0L);
+      verify(iterator, times(2)).next();
 
       queue.drivePrefetchOnce();
-      assertEquals(4, conversionCount.get());
-      assertEquals(5L, queue.getCurrentReadSearchIndex());
+      assertEquals(5, conversionCount.get());
+      assertEquals(6L, queue.getCurrentReadSearchIndex());
+      assertTrue(pendingEntries(queue).isEmpty());
+      assertEquals(0L, queue.getRetainedRequestBytes());
+      assertEquals(1L, queue.getPendingPathAcceptedEntries());
+      assertEquals(4L, queue.getWalPathAcceptedEntries());
+      verify(iterator, times(4)).next();
 
       queue.drivePrefetchOnce();
       assertEquals(5, conversionCount.get());
@@ -2571,9 +2578,12 @@ public class ConsensusPrefetchingQueueTest {
   @Test
   public void testMemoryBackpressureStopsFurtherMaterializationUntilAck() 
throws Exception {
     final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final int originalBatchMaxDelay =
+        
CommonDescriptor.getInstance().getConfig().getSubscriptionConsensusBatchMaxDelayInMs();
     final File systemDir = 
temporaryFolder.newFolder("system-memory-backpressure");
     ConsensusPrefetchingQueue queue = null;
     try {
+      
CommonDescriptor.getInstance().getConfig().setSubscriptionConsensusBatchMaxDelayInMs(0);
       final DataRegionId regionId = new DataRegionId(1);
       final FakeConsensusReqReader reader = new FakeConsensusReqReader();
       final IoTConsensusServerImpl serverImpl = 
mock(IoTConsensusServerImpl.class);
@@ -2632,41 +2642,34 @@ public class ConsensusPrefetchingQueueTest {
       assertEquals(oneTabletBytes, queue.getRetainedTabletBytes());
       assertTrue(memoryManager.getFreeMemorySizeInBytes() > 0L);
       assertEquals(1, queue.getPrefetchedEventCount());
-      assertTrue(pendingEntries(queue).isEmpty());
-      assertEquals("true", 
queue.coreReportMessage().get("realtimeAdmissionBlocked"));
-      assertFalse(pendingEntries(queue).offer(createRequest(4L)));
+      assertEquals(2, pendingEntries(queue).size());
+      assertTrue(queue.getRetainedRequestBytes() > 0L);
+      assertEquals("false", 
queue.coreReportMessage().get("realtimeAdmissionBlocked"));
+      assertTrue(pendingEntries(queue).offer(createRequest(4L)));
+      reader.currentSearchIndex = 4L;
 
       queue.drivePrefetchOnce();
       assertEquals(2, conversionCount.get());
       assertEquals(oneTabletBytes, queue.getRetainedTabletBytes());
 
-      final SubscriptionEvent event = queue.poll("consumer");
-      assertNotNull(event);
-      assertTrue(queue.ack("consumer", event.getCommitContext()));
-      assertEquals(0L, queue.getRetainedTabletBytes());
-
-      queue.drivePrefetchOnce();
-      assertEquals("false", 
queue.coreReportMessage().get("realtimeAdmissionBlocked"));
-      assertTrue(pendingEntries(queue).offer(secondRequest));
-      assertTrue(pendingEntries(queue).offer(thirdRequest));
-      queue.drivePrefetchOnce();
-      assertEquals(4, conversionCount.get());
-      assertEquals(3L, queue.getCurrentReadSearchIndex());
-      assertEquals(oneTabletBytes, queue.getRetainedTabletBytes());
-      assertEquals(1, queue.getPrefetchedEventCount());
-
-      final SubscriptionEvent secondEvent = queue.poll("consumer");
-      assertNotNull(secondEvent);
-      assertTrue(queue.ack("consumer", secondEvent.getCommitContext()));
-      queue.drivePrefetchOnce();
-      assertTrue(pendingEntries(queue).offer(thirdRequest));
-      queue.drivePrefetchOnce();
+      for (long index = 1L; index <= 4L; index++) {
+        final SubscriptionEvent event = queue.poll("consumer");
+        assertNotNull(event);
+        assertEquals(index, 
event.getCommitContext().getWriterProgress().getLocalSeq());
+        assertTrue(queue.ack("consumer", event.getCommitContext()));
+        assertEquals(0L, queue.getRetainedTabletBytes());
+        queue.drivePrefetchOnce();
+      }
 
-      assertEquals(5, conversionCount.get());
-      assertEquals(4L, queue.getCurrentReadSearchIndex());
+      assertEquals(7, conversionCount.get());
+      assertEquals(5L, queue.getCurrentReadSearchIndex());
       assertEquals("0", queue.coreReportMessage().get("pendingEntriesSize"));
       assertEquals("0", 
queue.coreReportMessage().get("bufferedRealtimeEntryCount"));
-      assertEquals(oneTabletBytes, queue.getRetainedTabletBytes());
+      assertEquals(0L, queue.getRetainedRequestBytes());
+      assertEquals(0L, queue.getRetainedTabletBytes());
+      assertEquals(4L, queue.getPendingPathAcceptedEntries());
+      assertEquals(0L, queue.getWalPathAcceptedEntries());
+      assertEquals(0L, queue.getRealtimeAdmissionRejectionCount());
 
       queue.close();
       queue = null;
@@ -2675,6 +2678,9 @@ public class ConsensusPrefetchingQueueTest {
       if (queue != null) {
         queue.close();
       }
+      CommonDescriptor.getInstance()
+          .getConfig()
+          .setSubscriptionConsensusBatchMaxDelayInMs(originalBatchMaxDelay);
       
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
     }
   }
@@ -2693,11 +2699,11 @@ public class ConsensusPrefetchingQueueTest {
     try {
       final int prefetchingQueueCapacity = getMaxPrefetchingQueueSize();
       final int requestCount = prefetchingQueueCapacity + 1;
-      
CommonDescriptor.getInstance().getConfig().setSubscriptionConsensusBatchMaxDelayInMs(0);
+      
CommonDescriptor.getInstance().getConfig().setSubscriptionConsensusBatchMaxDelayInMs(60_000);
       
CommonDescriptor.getInstance().getConfig().setSubscriptionConsensusBatchMaxTabletCount(1);
       CommonDescriptor.getInstance()
           .getConfig()
-          
.setSubscriptionConsensusBatchMaxWalEntries(prefetchingQueueCapacity);
+          .setSubscriptionConsensusBatchMaxWalEntries(requestCount);
 
       final DataRegionId regionId = new DataRegionId(1);
       final FakeConsensusReqReader reader = new FakeConsensusReqReader();
@@ -2734,25 +2740,30 @@ public class ConsensusPrefetchingQueueTest {
               true);
       queue.setSubscriptionMemoryManager(new SubscriptionMemoryManager(16L * 
1024 * 1024));
 
-      reader.currentSearchIndex = requestCount;
+      reader.currentSearchIndex = requestCount + 1L;
       assertNull(queue.poll("consumer"));
-      for (long searchIndex = 1L; searchIndex <= prefetchingQueueCapacity; 
searchIndex++) {
+      for (long searchIndex = 1L; searchIndex <= requestCount; searchIndex++) {
         assertTrue(pendingEntries(queue).offer(createRequest(searchIndex)));
       }
 
       queue.drivePrefetchOnce();
 
       assertEquals(prefetchingQueueCapacity, queue.getPrefetchedEventCount());
+      assertEquals(1, pendingEntries(queue).size());
+      assertTrue(queue.getRetainedRequestBytes() > 0L);
       assertEquals("false", 
queue.coreReportMessage().get("realtimeAdmissionBlocked"));
-      assertTrue(pendingEntries(queue).offer(createRequest(requestCount)));
+      assertTrue(pendingEntries(queue).offer(createRequest(requestCount + 
1L)));
 
       assertNotNull(queue.poll("consumer"));
       queue.drivePrefetchOnce();
 
       assertEquals(prefetchingQueueCapacity, queue.getPrefetchedEventCount());
       assertEquals(requestCount, conversionCount.get());
-      assertTrue(pendingEntries(queue).isEmpty());
+      assertEquals(1, pendingEntries(queue).size());
       assertEquals("false", 
queue.coreReportMessage().get("realtimeAdmissionBlocked"));
+      queue.close();
+      assertEquals(0L, queue.getRetainedRequestBytes());
+      queue = null;
     } finally {
       if (queue != null) {
         queue.close();
@@ -2833,16 +2844,18 @@ public class ConsensusPrefetchingQueueTest {
       assertEquals(oneTabletBytes * 2L, queue.getRetainedTabletBytes());
       assertTrue(queue.getRetainedTabletBytes() <= 
queue.getSubscriptionMemoryLimitInBytes());
       assertEquals(1, queue.getPrefetchedEventCount());
-      assertEquals("0", queue.coreReportMessage().get("pendingEntriesSize"));
+      assertEquals(
+          String.valueOf(writeCount - 2), 
queue.coreReportMessage().get("pendingEntriesSize"));
+      assertTrue(queue.getRetainedRequestBytes() > 0L);
       assertEquals("0", 
queue.coreReportMessage().get("bufferedRealtimeEntryCount"));
-      assertEquals("true", 
queue.coreReportMessage().get("realtimeAdmissionBlocked"));
+      assertEquals("false", 
queue.coreReportMessage().get("realtimeAdmissionBlocked"));
 
       for (int round = 0; round < backpressureVerificationRounds; round++) {
         queue.drivePrefetchOnce();
       }
       assertEquals(3, conversionCount.get());
       assertEquals(oneTabletBytes * 2L, queue.getRetainedTabletBytes());
-      assertFalse(pendingEntries(queue).offer(createRequest(writeCount + 1L)));
+      assertTrue(pendingEntries(queue).offer(createRequest(writeCount + 1L)));
 
       final SubscriptionEvent pausedEvent = queue.poll("pausedConsumer");
       assertNotNull(pausedEvent);
@@ -2853,8 +2866,10 @@ public class ConsensusPrefetchingQueueTest {
       }
       assertEquals(3, conversionCount.get());
       assertEquals(oneTabletBytes * 2L, queue.getRetainedTabletBytes());
+      assertEquals(writeCount - 1, pendingEntries(queue).size());
 
       queue.close();
+      assertEquals(0L, queue.getRetainedRequestBytes());
       queue = null;
       assertEquals(0L, memoryManager.getUsedMemorySizeInBytes());
     } finally {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
index 377f20801c4..192ed7c3e9c 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWalBackpressureTest.java
@@ -197,7 +197,7 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
   }
 
   @Test
-  public void testAckRecoversDrainedSuffixFromWalWithoutReoffer() throws 
Exception {
+  public void testAckResumesPendingSuffixWithoutWalReplay() throws Exception {
     final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
     final CommonConfig config = CommonDescriptor.getInstance().getConfig();
     final int originalBatchMaxWalEntries = 
config.getSubscriptionConsensusBatchMaxWalEntries();
@@ -213,7 +213,7 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
       config.setSubscriptionConsensusBatchMaxWalEntries(128);
       config.setSubscriptionConsensusBatchMaxTabletCount(128);
       config.setSubscriptionConsensusBatchMaxSizeInBytes(oneTabletBytes * 16L);
-      config.setSubscriptionConsensusBatchMaxDelayInMs(60_000);
+      config.setSubscriptionConsensusBatchMaxDelayInMs(0);
 
       writeSealedWal(walDirectory);
 
@@ -272,8 +272,9 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
       assertEquals(Arrays.asList(1L, 2L), conversionAttempts);
       assertEquals(2L, queue.getCurrentReadSearchIndex());
       assertEquals(1, queue.getPrefetchedEventCount());
-      assertEquals(0, pendingEntries(queue).size());
-      assertEquals("true", 
queue.coreReportMessage().get("realtimeAdmissionBlocked"));
+      assertEquals(2, pendingEntries(queue).size());
+      assertTrue(queue.getRetainedRequestBytes() > 0L);
+      assertEquals("false", 
queue.coreReportMessage().get("realtimeAdmissionBlocked"));
       assertEquals(oneTabletBytes, memoryManager.getUsedMemorySizeInBytes());
       assertMemoryBounded(queue, memoryManager);
 
@@ -302,8 +303,10 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
 
       assertEquals(Arrays.asList(1L, 2L, 3L), deliveredLocalSequences);
       assertEquals(Arrays.asList(1L, 2L, 2L, 3L, 3L), conversionAttempts);
-      assertEquals(1L, queue.getPendingPathAcceptedEntries());
-      assertEquals(2L, queue.getWalPathAcceptedEntries());
+      assertEquals(3L, queue.getPendingPathAcceptedEntries());
+      assertEquals(0L, queue.getWalPathAcceptedEntries());
+      assertEquals(0L, queue.getRetainedRequestBytes());
+      assertEquals(0L, queue.getRealtimeAdmissionRejectionCount());
       assertEquals(4L, queue.getCurrentReadSearchIndex());
       assertEquals(0, queue.getPrefetchedEventCount());
       assertEquals(0L, queue.getSubscriptionUncommittedEventCount());
@@ -553,7 +556,8 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
       assertNull(queue.poll("consumer"));
       // Admission failures leave a hole before newer pending requests. Keep a 
suffix queued so
       // every bounded round enters fillGapFromWAL, including after ACK 
releases Tablet memory.
-      for (long index = replayedRequests + 1L; index <= requestCount; index++) 
{
+      // Leave the gap unresolved through the last round so this test measures 
only WAL work.
+      for (long index = replayedRequests + 3L; index <= requestCount; index++) 
{
         assertTrue(pendingEntries(queue).offer(createRequest(index)));
       }
 
@@ -594,6 +598,8 @@ public class ConsensusPrefetchingQueueWalBackpressureTest {
       assertEquals(0L, queue.getWalGapSkippedEntries());
       assertEquals(0L, queue.getSubscriptionUncommittedEventCount());
       assertEquals(0L, queue.getRetainedTabletBytes());
+      assertEquals(2 * maxRounds - 2, pendingEntries(queue).size());
+      assertTrue(queue.getRetainedRequestBytes() > 0L);
       if (memoryBackpressure) {
         assertTrue(queue.getSubscriptionMemoryRejectionCount() > 0L);
       }

Reply via email to