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 0c9fb83974f [Fix] Prevent silent subscription data loss on WAL replay 
gaps (#18741)
0c9fb83974f is described below

commit 0c9fb83974fd00470506bb82a4563c127e195cde
Author: Caideyipi <[email protected]>
AuthorDate: Wed Sep 30 19:46:25 2026 +0800

    [Fix] Prevent silent subscription data loss on WAL replay gaps (#18741)
    
    * Fix silent subscription data loss on WAL replay gaps
    
    * Fix transient WAL visibility gaps during subscription replay
    
    * Fix subscription WAL retention at replay cursor
    
    * Fix duplicate WAL metadata test helper
---
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |  11 +-
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |   9 +-
 .../consensus/ConsensusPrefetchingQueue.java       | 110 +++++++++++--
 .../consensus/ConsensusPrefetchingQueueTest.java   | 172 +++++++++++++++++++++
 4 files changed, 278 insertions(+), 24 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..95583ded3a9 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
@@ -2216,10 +2216,13 @@ public final class DataNodePipeMessages {
       "ProgressWALIterator: skipped {} unreadable retained WAL files in 
directory {}, "
           + "firstFile={}, lastFile={}, firstError={}; historical subscription 
data in these "
           + "files cannot be replayed";
-  public static final String 
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_WAL_REPLAY_SKIPPED_UNAVAILABLE_SEARCH_INDEXES_B8023B64
 =
-      "ConsensusPrefetchingQueue {}: WAL replay skipped unavailable search 
indexes [{}, {}), "
-          + "skippedEntries={}, totalWalGapSkippedEntries={}; the missing WAL 
data may have been "
-          + "reclaimed before subscription consumption";
+  public static final String 
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_WAL_REPLAY_FOUND_UNAVAILABLE_SEARCH_INDEXES_E0CBFFFA
 =
+      "ConsensusPrefetchingQueue {}: WAL replay found unavailable search 
indexes [{}, {}) before "
+          + "searchIndex {}; forcing WAL refresh before retry";
+  public static final String 
MESSAGE_CONSENSUSPREFETCHINGQUEUE_WAL_REPLAY_CANNOT_RECOVER_SEARCH_INDEXES_70781B22
 =
+      "ConsensusPrefetchingQueue %s: WAL replay cannot recover search indexes 
[%s, %s), "
+          + "unavailableEntries=%s, totalWalGapSkippedEntries=%s; subscription 
delivery is "
+          + "stalled to prevent silent data loss";
   public static final String 
PIPE_LOG_PIPE_TERMINATE_EVENT_COMMITTED_FOR_HISTORICAL_TRANSFER_CREATIONTIME_9B807B28
 =
       "Pipe {}@{}: terminate event committed for historical transfer. 
creationTime: {}, "
           + "shouldMark: {}. {}";
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..ccb9cd42da2 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
@@ -2057,9 +2057,12 @@ public final class DataNodePipeMessages {
   public static final String 
PIPE_LOG_PROGRESSWALITERATOR_SKIPPED_UNREADABLE_RETAINED_WAL_FILES_FFC8455E =
       "ProgressWALIterator:跳过了 {} 个无法读取的保留 WAL 文件,directory={},firstFile={},"
           + "lastFile={},firstError={};这些文件中的历史订阅数据无法重放";
-  public static final String 
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_WAL_REPLAY_SKIPPED_UNAVAILABLE_SEARCH_INDEXES_B8023B64
 =
-      "ConsensusPrefetchingQueue {}:WAL 重放跳过了不可用的 searchIndex 区间 [{}, {}),"
-          + "skippedEntries={},totalWalGapSkippedEntries={};缺失的 WAL 
数据可能已在订阅消费前被回收";
+  public static final String 
PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_WAL_REPLAY_FOUND_UNAVAILABLE_SEARCH_INDEXES_E0CBFFFA
 =
+      "ConsensusPrefetchingQueue {}:WAL 回放发现不可用 searchIndex 区间 [{}, {}),下一个可见 "
+          + "searchIndex={};正在强制刷新 WAL 后重试";
+  public static final String 
MESSAGE_CONSENSUSPREFETCHINGQUEUE_WAL_REPLAY_CANNOT_RECOVER_SEARCH_INDEXES_70781B22
 =
+      "ConsensusPrefetchingQueue %s:WAL 回放无法恢复 searchIndex 区间 [%s, 
%s),不可用条目数=%s,"
+          + "walGapSkippedEntries 总数=%s;为防止静默丢数,订阅投递已停滞";
   public static final String 
PIPE_LOG_PIPE_TERMINATE_EVENT_COMMITTED_FOR_HISTORICAL_TRANSFER_CREATIONTIME_9B807B28
 =
       "Pipe {}@{}:历史传输的终止事件已提交。creationTime:{},shouldMark:{}。{}";
   public static final String 
PIPE_LOG_PIPE_HISTORICAL_SOURCE_HAS_SUPPLIED_ALL_EVENTS_EMITTING_8B58DE19 =
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 0dcddbef7a2..c5641db1f18 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
@@ -297,6 +297,12 @@ public class ConsensusPrefetchingQueue {
 
   private volatile long lastWalGapWaitLogTimeMs = 0L;
 
+  /** Search index whose WAL visibility gap is being retried after forcing a 
WAL refresh. */
+  private volatile long walGapRetryExpectedSearchIndex = Long.MIN_VALUE;
+
+  /** Critical replay failure returned to consumers instead of silently 
advancing past data. */
+  private volatile String walReplayFailureMessage;
+
   /** Fallback committed region progress from local persisted state. */
   private final RegionProgress fallbackCommittedRegionProgress;
 
@@ -771,6 +777,9 @@ public class ConsensusPrefetchingQueue {
       if (pendingSeekRequest != null) {
         return null;
       }
+      if (Objects.nonNull(walReplayFailureMessage)) {
+        return generateCriticalErrorResponse(walReplayFailureMessage);
+      }
       final SubscriptionEvent event = pollInternal(consumerId);
       if (Objects.nonNull(event) && prefetchingQueue.size() < 
MAX_PREFETCHING_QUEUE_SIZE) {
         requestPrefetch();
@@ -837,6 +846,7 @@ public class ConsensusPrefetchingQueue {
     // readers without WAL support.
     this.subscriptionWALIterator =
         createSubscriptionWALIterator(resolvedStart.getStartSearchIndex());
+    resetWalReplayFailureState();
     this.prefetchInitialized = true;
     this.observedSeekGeneration = seekGeneration.get();
     discardBatch(this.lingerBatch);
@@ -1427,6 +1437,11 @@ public class ConsensusPrefetchingQueue {
       applyPendingSubscriptionWalReset(observedSeekGeneration);
       recycleInFlightEvents();
 
+      if (Objects.nonNull(walReplayFailureMessage)) {
+        blockRealtimeAdmission();
+        return PrefetchRoundResult.dormant();
+      }
+
       if (!isActive) {
         blockRealtimeAdmission();
         return computeIdleRoundResult();
@@ -1511,6 +1526,11 @@ public class ConsensusPrefetchingQueue {
 
       if (batch.isEmpty() && lingerBatch.isEmpty()) {
         final MaterializationResult walResult = 
tryCatchUpFromWAL(observedSeekGeneration);
+        if (walResult == MaterializationResult.WAL_GAP) {
+          return Objects.nonNull(walReplayFailureMessage)
+              ? PrefetchRoundResult.dormant()
+              : PrefetchRoundResult.rescheduleAfter(WAL_GAP_RETRY_SLEEP_MS);
+        }
         if (walResult == MaterializationResult.MEMORY_BLOCKED) {
           blockRealtimeAdmission();
           return PrefetchRoundResult.rescheduleAfter(MEMORY_RETRY_SLEEP_MS);
@@ -1926,7 +1946,9 @@ public class ConsensusPrefetchingQueue {
         pumpFromSubscriptionWAL(
             batchState, expectedSeekGeneration, maxWalEntries, maxTablets, 
maxBatchBytes);
     if (materializationResult != MaterializationResult.SUCCESS) {
-      if (materializationResult == MaterializationResult.MEMORY_BLOCKED && 
!batchState.isEmpty()) {
+      if ((materializationResult == MaterializationResult.MEMORY_BLOCKED
+              || materializationResult == MaterializationResult.WAL_GAP)
+          && !batchState.isEmpty()) {
         if (!flushBatch(batchState, expectedSeekGeneration)) {
           discardBatch(batchState);
           return MaterializationResult.STALE;
@@ -1969,6 +1991,10 @@ public class ConsensusPrefetchingQueue {
         if (isBeforeLocalCursor(walEntry)) {
           continue;
         }
+        final MaterializationResult continuityResult = 
validateWalReplayContinuity(walEntry);
+        if (continuityResult != MaterializationResult.SUCCESS) {
+          return continuityResult;
+        }
         if (shouldSkipForRecoveryProgress(walEntry)) {
           advanceWalReplayCursorIfPresent(walEntry);
           continue;
@@ -2031,22 +2057,67 @@ public class ConsensusPrefetchingQueue {
     if (!hasLocalSearchIndex(request)) {
       return;
     }
+    nextExpectedSearchIndex.set(request.getSearchIndex() + 1);
+  }
+
+  private MaterializationResult validateWalReplayContinuity(final 
IndexedConsensusRequest request) {
+    if (!hasLocalSearchIndex(request)) {
+      return MaterializationResult.SUCCESS;
+    }
 
     final long actualSearchIndex = request.getSearchIndex();
     final long expectedSearchIndex = nextExpectedSearchIndex.get();
-    if (actualSearchIndex > expectedSearchIndex) {
-      final long skippedEntries = actualSearchIndex - expectedSearchIndex;
-      final long totalSkippedEntries = 
walGapSkippedEntries.addAndGet(skippedEntries);
+    if (actualSearchIndex <= expectedSearchIndex) {
+      if (actualSearchIndex == expectedSearchIndex
+          && walGapRetryExpectedSearchIndex == expectedSearchIndex) {
+        walGapRetryExpectedSearchIndex = Long.MIN_VALUE;
+        pendingWalGapRetryRequested = false;
+      }
+      return MaterializationResult.SUCCESS;
+    }
+
+    if (walGapRetryExpectedSearchIndex != expectedSearchIndex) {
+      walGapRetryExpectedSearchIndex = expectedSearchIndex;
       LOGGER.warn(
           DataNodePipeMessages
-              
.PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_WAL_REPLAY_SKIPPED_UNAVAILABLE_SEARCH_INDEXES_B8023B64,
+              
.PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_WAL_REPLAY_FOUND_UNAVAILABLE_SEARCH_INDEXES_E0CBFFFA,
           this,
           expectedSearchIndex,
           actualSearchIndex,
-          skippedEntries,
-          totalSkippedEntries);
+          actualSearchIndex);
+      if (consensusReqReader instanceof WALNode) {
+        ((WALNode) consensusReqReader).rollWALFile();
+      }
+      resetSubscriptionWALPosition(expectedSearchIndex);
+      onWalGapRetryScheduled();
+      pendingWalGapRetryRequested = true;
+      return MaterializationResult.WAL_GAP;
+    }
+
+    if (Objects.isNull(walReplayFailureMessage)) {
+      final long unavailableEntries = actualSearchIndex - expectedSearchIndex;
+      final long totalUnavailableEntries = 
walGapSkippedEntries.addAndGet(unavailableEntries);
+      walReplayFailureMessage =
+          String.format(
+              DataNodePipeMessages
+                  
.MESSAGE_CONSENSUSPREFETCHINGQUEUE_WAL_REPLAY_CANNOT_RECOVER_SEARCH_INDEXES_70781B22,
+              this,
+              expectedSearchIndex,
+              actualSearchIndex,
+              unavailableEntries,
+              totalUnavailableEntries);
+      LOGGER.error(walReplayFailureMessage);
+      blockRealtimeAdmission();
     }
-    nextExpectedSearchIndex.set(actualSearchIndex + 1);
+    return MaterializationResult.WAL_GAP;
+  }
+
+  private void resetWalReplayFailureState() {
+    pendingWalGapRetryRequested = false;
+    walGapWaitStartTimeMs = 0L;
+    lastWalGapWaitLogTimeMs = 0L;
+    walGapRetryExpectedSearchIndex = Long.MIN_VALUE;
+    walReplayFailureMessage = null;
   }
 
   private void ensureSubscriptionWalReadable() {
@@ -3049,9 +3120,7 @@ public class ConsensusPrefetchingQueue {
       reconcileRetainedTabletMemoryAfterCleanup();
       memoryBlockedEntryBytes = -1L;
       resetBatchWriterProgress();
-      pendingWalGapRetryRequested = false;
-      walGapWaitStartTimeMs = 0L;
-      lastWalGapWaitLogTimeMs = 0L;
+      resetWalReplayFailureState();
       pendingSubscriptionWalResetSearchIndex = Long.MIN_VALUE;
       pendingSubscriptionWalResetGeneration = Long.MIN_VALUE;
       closeSubscriptionWALIterator();
@@ -3345,9 +3414,7 @@ public class ConsensusPrefetchingQueue {
     reconcileRetainedTabletMemoryAfterCleanup();
     resetBatchWriterProgress();
     observedSeekGeneration = seekGeneration.get();
-    pendingWalGapRetryRequested = false;
-    walGapWaitStartTimeMs = 0L;
-    lastWalGapWaitLogTimeMs = 0L;
+    resetWalReplayFailureState();
 
     // 5. Reset commit state to the writer progress immediately before the 
first re-delivered
     // entry so seek/rebind resumes from the intended frontier.
@@ -3448,6 +3515,7 @@ public class ConsensusPrefetchingQueue {
       return 0L;
     }
 
+    WALFileUtils.ascSortByVersionId(walFiles);
     final int replayFileIndex =
         Math.max(0, WALFileUtils.binarySearchFileBySearchIndex(walFiles, 
nextExpectedSearchIndex));
     return WALFileUtils.parseVersionId(walFiles[replayFileIndex].getName());
@@ -3960,6 +4028,13 @@ public class ConsensusPrefetchingQueue {
         
createNonCommittableContext(IoTDBDescriptor.getInstance().getConfig().getDataNodeId()));
   }
 
+  private SubscriptionEvent generateCriticalErrorResponse(final String 
errorMessage) {
+    return new SubscriptionEvent(
+        SubscriptionPollResponseType.ERROR.getType(),
+        new ErrorPayload(errorMessage, true),
+        
createNonCommittableContext(IoTDBDescriptor.getInstance().getConfig().getDataNodeId()));
+  }
+
   private SubscriptionEvent generateOutdatedErrorResponse() {
     return new SubscriptionEvent(
         SubscriptionPollResponseType.ERROR.getType(),
@@ -4064,9 +4139,7 @@ public class ConsensusPrefetchingQueue {
           memoryBlockedEntryBytes = -1L;
           prefetchInitialized = false;
           observedSeekGeneration = seekGeneration.get();
-          pendingWalGapRetryRequested = false;
-          walGapWaitStartTimeMs = 0L;
-          lastWalGapWaitLogTimeMs = 0L;
+          resetWalReplayFailureState();
           pendingSubscriptionWalResetSearchIndex = Long.MIN_VALUE;
           pendingSubscriptionWalResetGeneration = Long.MIN_VALUE;
           closeSubscriptionWALIterator();
@@ -4154,6 +4227,9 @@ public class ConsensusPrefetchingQueue {
     if (!isActive) {
       return SubscriptionProgressSnapshot.STATUS_INACTIVE;
     }
+    if (Objects.nonNull(walReplayFailureMessage)) {
+      return SubscriptionProgressSnapshot.STATUS_STALLED;
+    }
     if (getLag() <= 0L) {
       return SubscriptionProgressSnapshot.STATUS_CAUGHT_UP;
     }
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 6efb3298bf9..a854b8ad6dc 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
@@ -1382,6 +1382,92 @@ public class ConsensusPrefetchingQueueTest {
     }
   }
 
+  @Test
+  public void testWalReplayRetriesGapWithoutSkippingEntries() 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 WALNode walNode = mock(WALNode.class);
+      when(walNode.getCurrentSearchIndex()).thenReturn(4L);
+      when(walNode.getLogDirectory()).thenReturn(systemDir);
+      final IoTConsensusServerImpl serverImpl = 
mock(IoTConsensusServerImpl.class);
+      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()))
+          .thenAnswer(
+              ignored -> {
+                conversionCount.incrementAndGet();
+                return Collections.singletonList(createTablet());
+              });
+      when(converter.getDatabaseName()).thenReturn("db");
+
+      final Iterator<IndexedConsensusRequest> initiallyVisibleEntries =
+          Arrays.asList(createRequest(1L), createRequest(4L)).iterator();
+      final ProgressWALIterator initialIterator = 
mock(ProgressWALIterator.class);
+      when(initialIterator.hasNext()).thenAnswer(ignored -> 
initiallyVisibleEntries.hasNext());
+      when(initialIterator.next()).thenAnswer(ignored -> 
initiallyVisibleEntries.next());
+      final Iterator<IndexedConsensusRequest> refreshedEntries =
+          Arrays.asList(createRequest(2L), createRequest(3L), 
createRequest(4L)).iterator();
+      final ProgressWALIterator refreshedIterator = 
mock(ProgressWALIterator.class);
+      when(refreshedIterator.hasNext()).thenAnswer(ignored -> 
refreshedEntries.hasNext());
+      when(refreshedIterator.next()).thenAnswer(ignored -> 
refreshedEntries.next());
+      final AtomicInteger iteratorCreationCount = new AtomicInteger();
+
+      queue =
+          new ConsensusPrefetchingQueue(
+              "consumerGroup",
+              "topic",
+              TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+              regionId,
+              serverImpl,
+              new SubscriptionWalRetentionPolicy(
+                  "topic",
+                  SubscriptionWalRetentionPolicy.UNBOUNDED,
+                  SubscriptionWalRetentionPolicy.UNBOUNDED),
+              converter,
+              newCommitManager(systemDir),
+              new RegionProgress(Collections.emptyMap()),
+              1L,
+              1L,
+              true) {
+            @Override
+            protected ProgressWALIterator createSubscriptionWALIterator(
+                final long startSearchIndex) {
+              return iteratorCreationCount.getAndIncrement() == 0
+                  ? initialIterator
+                  : refreshedIterator;
+            }
+          };
+      queue.setSubscriptionMemoryManager(new SubscriptionMemoryManager(16L * 
1024 * 1024));
+
+      assertNull(queue.poll("consumer"));
+      queue.drivePrefetchOnce();
+
+      assertEquals(1L, queue.getWalPathAcceptedEntries());
+      assertEquals(1, conversionCount.get());
+      assertEquals(0L, queue.getWalGapSkippedEntries());
+      assertEquals(2L, queue.getCurrentReadSearchIndex());
+      verify(walNode).rollWALFile();
+
+      queue.drivePrefetchOnce();
+
+      assertEquals(4L, queue.getWalPathAcceptedEntries());
+      assertEquals(4, conversionCount.get());
+      assertEquals(0L, queue.getWalGapSkippedEntries());
+      assertEquals(5L, queue.getCurrentReadSearchIndex());
+    } finally {
+      if (queue != null) {
+        queue.close();
+      }
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
   @Test
   public void testPendingGapReplayHonorsPerRoundWalEntryLimit() throws 
Exception {
     final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
@@ -1523,6 +1609,92 @@ public class ConsensusPrefetchingQueueTest {
     }
   }
 
+  @Test
+  public void testWalReplayFailsCriticallyWhenGapRemainsUnavailable() throws 
Exception {
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final File systemDir = 
temporaryFolder.newFolder("wal-replay-unrecoverable-gap");
+    ConsensusPrefetchingQueue queue = null;
+    try {
+      final DataRegionId regionId = new DataRegionId(10);
+      final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+      reader.currentSearchIndex = 4L;
+      final IoTConsensusServerImpl serverImpl = 
mock(IoTConsensusServerImpl.class);
+      when(serverImpl.getConsensusReqReader()).thenReturn(reader);
+      when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new 
WriterSafeFrontierTracker());
+
+      final AtomicInteger conversionCount = new AtomicInteger();
+      final ConsensusLogToTabletConverter converter = 
mock(ConsensusLogToTabletConverter.class);
+      when(converter.convert(any()))
+          .thenAnswer(
+              ignored -> {
+                conversionCount.incrementAndGet();
+                return Collections.singletonList(createTablet());
+              });
+      when(converter.getDatabaseName()).thenReturn("db");
+
+      final Iterator<IndexedConsensusRequest> initiallyVisibleEntries =
+          Arrays.asList(createRequest(1L), createRequest(4L)).iterator();
+      final ProgressWALIterator initialIterator = 
mock(ProgressWALIterator.class);
+      when(initialIterator.hasNext()).thenAnswer(ignored -> 
initiallyVisibleEntries.hasNext());
+      when(initialIterator.next()).thenAnswer(ignored -> 
initiallyVisibleEntries.next());
+      final Iterator<IndexedConsensusRequest> refreshedEntries =
+          Collections.singletonList(createRequest(4L)).iterator();
+      final ProgressWALIterator refreshedIterator = 
mock(ProgressWALIterator.class);
+      when(refreshedIterator.hasNext()).thenAnswer(ignored -> 
refreshedEntries.hasNext());
+      when(refreshedIterator.next()).thenAnswer(ignored -> 
refreshedEntries.next());
+      final AtomicInteger iteratorCreationCount = new AtomicInteger();
+
+      queue =
+          new ConsensusPrefetchingQueue(
+              "consumerGroup",
+              "topic",
+              TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+              regionId,
+              serverImpl,
+              new SubscriptionWalRetentionPolicy(
+                  "topic",
+                  SubscriptionWalRetentionPolicy.UNBOUNDED,
+                  SubscriptionWalRetentionPolicy.UNBOUNDED),
+              converter,
+              newCommitManager(systemDir),
+              new RegionProgress(Collections.emptyMap()),
+              1L,
+              1L,
+              true) {
+            @Override
+            protected ProgressWALIterator createSubscriptionWALIterator(
+                final long startSearchIndex) {
+              return iteratorCreationCount.getAndIncrement() == 0
+                  ? initialIterator
+                  : refreshedIterator;
+            }
+          };
+      queue.setSubscriptionMemoryManager(new SubscriptionMemoryManager(16L * 
1024 * 1024));
+
+      assertNull(queue.poll("consumer"));
+      queue.drivePrefetchOnce();
+      queue.drivePrefetchOnce();
+
+      assertEquals(1L, queue.getWalPathAcceptedEntries());
+      assertEquals(1, conversionCount.get());
+      assertEquals(2L, queue.getWalGapSkippedEntries());
+      assertEquals(2L, queue.getCurrentReadSearchIndex());
+      assertEquals(4L, queue.getProgressStatus());
+
+      final SubscriptionEvent errorEvent = queue.poll("consumer");
+      assertNotNull(errorEvent);
+      assertEquals(
+          SubscriptionPollResponseType.ERROR.getType(),
+          errorEvent.getCurrentResponse().getResponseType());
+      assertTrue(((ErrorPayload) 
errorEvent.getCurrentResponse().getPayload()).isCritical());
+    } finally {
+      if (queue != null) {
+        queue.close();
+      }
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
   @Test
   public void 
testActivationRetriesUntilConfigNodeProgressIsExplicitlyAvailable() throws 
Exception {
     final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();

Reply via email to