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 abd499334f0 [Subscription] Bound pending WAL gap replay per round 
(#18766)
abd499334f0 is described below

commit abd499334f024eb36379582d6c3b60b2690ed062
Author: Caideyipi <[email protected]>
AuthorDate: Wed Sep 30 10:16:08 2026 +0800

    [Subscription] Bound pending WAL gap replay per round (#18766)
    
    * [Subscription] Bound pending WAL gap replay per round
    
    * [Subscription] Stabilize WAL gap counter regression test
---
 .../consensus/ConsensusPrefetchingQueue.java       |  16 ++-
 .../consensus/ConsensusPrefetchingQueueTest.java   | 108 ++++++++++++++++++---
 2 files changed, 108 insertions(+), 16 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 d415656986b..149d968796e 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
@@ -1481,7 +1481,12 @@ public class ConsensusPrefetchingQueue {
         try {
           batchResult =
               accumulateFromPending(
-                  batch, lingerBatch, observedSeekGeneration, maxTablets, 
maxBatchBytes);
+                  batch,
+                  lingerBatch,
+                  observedSeekGeneration,
+                  maxWalEntries,
+                  maxTablets,
+                  maxBatchBytes);
         } finally {
           pendingEntries.release(batch);
         }
@@ -1758,6 +1763,7 @@ public class ConsensusPrefetchingQueue {
       final List<IndexedConsensusRequest> batch,
       final DeliveryBatchState lingerBatch,
       final long expectedSeekGeneration,
+      final int maxWalEntries,
       final int maxTablets,
       final long maxBatchBytes) {
 
@@ -1784,6 +1790,7 @@ public class ConsensusPrefetchingQueue {
                 searchIndex,
                 lingerBatch,
                 expectedSeekGeneration,
+                maxWalEntries,
                 maxTablets,
                 maxBatchBytes);
         if (gapFillResult != MaterializationResult.SUCCESS) {
@@ -1850,6 +1857,7 @@ public class ConsensusPrefetchingQueue {
       final long toIndex,
       final DeliveryBatchState batchState,
       final long expectedSeekGeneration,
+      final int maxWalEntries,
       final int maxTablets,
       final long maxBatchBytes) {
     pendingWalGapRetryRequested = false;
@@ -1857,9 +1865,13 @@ public class ConsensusPrefetchingQueue {
     if (seekGeneration.get() != expectedSeekGeneration || isClosed) {
       return MaterializationResult.STALE;
     }
+    // A pending-queue jump can span millions of WAL entries when real-time 
delivery has overflowed.
+    // Keep gap recovery within the normal per-round budget so sparse topic 
filtering cannot hold
+    // the
+    // queue read lock indefinitely and block unsubscribe from acquiring the 
close write lock.
     final MaterializationResult pumpResult =
         pumpFromSubscriptionWAL(
-            batchState, expectedSeekGeneration, Integer.MAX_VALUE, maxTablets, 
maxBatchBytes);
+            batchState, expectedSeekGeneration, maxWalEntries, maxTablets, 
maxBatchBytes);
     if (pumpResult != MaterializationResult.SUCCESS) {
       return pumpResult;
     }
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 69ece3840ef..9d85d6b9d57 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
@@ -1178,27 +1178,44 @@ public class ConsensusPrefetchingQueueTest {
   }
 
   @Test
-  public void testWalReplayCountsOnlyUnavailableSearchIndexes() throws 
Exception {
+  public void testPendingGapReplayHonorsPerRoundWalEntryLimit() throws 
Exception {
     final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
-    final File systemDir = temporaryFolder.newFolder("wal-replay-gap-counter");
+    final int originalBatchMaxWalEntries =
+        
CommonDescriptor.getInstance().getConfig().getSubscriptionConsensusBatchMaxWalEntries();
+    final File systemDir = 
temporaryFolder.newFolder("bounded-pending-gap-replay");
     ConsensusPrefetchingQueue queue = null;
     try {
-      final DataRegionId regionId = new DataRegionId(9);
-      final FakeConsensusReqReader reader = new FakeConsensusReqReader();
-      reader.currentSearchIndex = 4L;
+      
CommonDescriptor.getInstance().getConfig().setSubscriptionConsensusBatchMaxWalEntries(2);
+
+      final DataRegionId regionId = new DataRegionId(12);
+      final WALNode walNode = mock(WALNode.class);
+      when(walNode.getCurrentSearchIndex()).thenReturn(5L);
+      when(walNode.getLogDirectory()).thenReturn(systemDir);
       final IoTConsensusServerImpl serverImpl = 
mock(IoTConsensusServerImpl.class);
-      when(serverImpl.getConsensusReqReader()).thenReturn(reader);
+      when(serverImpl.getConsensusReqReader()).thenReturn(walNode);
       when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new 
WriterSafeFrontierTracker());
 
+      final AtomicInteger conversionCount = new AtomicInteger();
       final ConsensusLogToTabletConverter converter = 
mock(ConsensusLogToTabletConverter.class);
-      
when(converter.convert(any())).thenReturn(Collections.singletonList(createTablet()));
+      when(converter.convert(any()))
+          .thenAnswer(
+              ignored -> {
+                conversionCount.incrementAndGet();
+                return Collections.emptyList();
+              });
       when(converter.getDatabaseName()).thenReturn("db");
 
-      final Iterator<IndexedConsensusRequest> retainedWalEntries =
-          Arrays.asList(createRequest(1L), createRequest(4L)).iterator();
-      final ProgressWALIterator walIterator = mock(ProgressWALIterator.class);
-      when(walIterator.hasNext()).thenAnswer(ignored -> 
retainedWalEntries.hasNext());
-      when(walIterator.next()).thenAnswer(ignored -> 
retainedWalEntries.next());
+      final Iterator<IndexedConsensusRequest> walEntries =
+          Arrays.asList(
+                  createRequest(1L),
+                  createRequest(2L),
+                  createRequest(3L),
+                  createRequest(4L),
+                  createRequest(5L))
+              .iterator();
+      final ProgressWALIterator iterator = mock(ProgressWALIterator.class);
+      when(iterator.hasNext()).thenAnswer(ignored -> walEntries.hasNext());
+      when(iterator.next()).thenAnswer(ignored -> walEntries.next());
 
       queue =
           new ConsensusPrefetchingQueue(
@@ -1220,14 +1237,77 @@ public class ConsensusPrefetchingQueueTest {
             @Override
             protected ProgressWALIterator createSubscriptionWALIterator(
                 final long startSearchIndex) {
-              return walIterator;
+              return iterator;
             }
           };
+      queue.setSubscriptionMemoryManager(new SubscriptionMemoryManager(16L * 
1024 * 1024));
 
       assertNull(queue.poll("consumer"));
+      assertTrue(pendingEntries(queue).offer(createRequest(5L)));
+
+      queue.drivePrefetchOnce();
+      assertEquals(2, conversionCount.get());
+      assertEquals(3L, queue.getCurrentReadSearchIndex());
+      assertTrue(pendingEntries(queue).isEmpty());
+
       queue.drivePrefetchOnce();
+      assertEquals(4, conversionCount.get());
+      assertEquals(5L, queue.getCurrentReadSearchIndex());
+
+      queue.drivePrefetchOnce();
+      assertEquals(5, conversionCount.get());
+      assertEquals(6L, queue.getCurrentReadSearchIndex());
+    } finally {
+      if (queue != null) {
+        queue.close();
+      }
+      CommonDescriptor.getInstance()
+          .getConfig()
+          
.setSubscriptionConsensusBatchMaxWalEntries(originalBatchMaxWalEntries);
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
+  @Test
+  public void testWalReplayCountsOnlyUnavailableSearchIndexes() throws 
Exception {
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final File systemDir = temporaryFolder.newFolder("wal-replay-gap-counter");
+    ConsensusPrefetchingQueue queue = null;
+    try {
+      final DataRegionId regionId = new DataRegionId(9);
+      final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+      final IoTConsensusServerImpl serverImpl = 
mock(IoTConsensusServerImpl.class);
+      when(serverImpl.getConsensusReqReader()).thenReturn(reader);
+      when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new 
WriterSafeFrontierTracker());
+
+      queue =
+          new ConsensusPrefetchingQueue(
+              "consumerGroup",
+              "topic",
+              TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+              regionId,
+              serverImpl,
+              new SubscriptionWalRetentionPolicy(
+                  "topic",
+                  SubscriptionWalRetentionPolicy.UNBOUNDED,
+                  SubscriptionWalRetentionPolicy.UNBOUNDED),
+              mock(ConsensusLogToTabletConverter.class),
+              newCommitManager(systemDir),
+              new RegionProgress(Collections.emptyMap()),
+              1L,
+              1L,
+              true);
+
+      final Method advanceWalReplayCursorIfPresent =
+          ConsensusPrefetchingQueue.class.getDeclaredMethod(
+              "advanceWalReplayCursorIfPresent", 
IndexedConsensusRequest.class);
+      advanceWalReplayCursorIfPresent.setAccessible(true);
+
+      advanceWalReplayCursorIfPresent.invoke(queue, createRequest(1L));
+      assertEquals(0L, queue.getWalGapSkippedEntries());
+      assertEquals(2L, queue.getCurrentReadSearchIndex());
 
-      assertEquals(2L, queue.getWalPathAcceptedEntries());
+      advanceWalReplayCursorIfPresent.invoke(queue, createRequest(4L));
       assertEquals(2L, queue.getWalGapSkippedEntries());
       assertEquals(5L, queue.getCurrentReadSearchIndex());
     } finally {

Reply via email to