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 99a3e06c76b [Fix] Preserve absent local search index during 
subscription WAL replay (#18805)
99a3e06c76b is described below

commit 99a3e06c76b6b47c6d369632cc576a958d99e311
Author: Caideyipi <[email protected]>
AuthorDate: Sat Oct 10 10:35:21 2026 +0800

    [Fix] Preserve absent local search index during subscription WAL replay 
(#18805)
    
    * Fix subscription WAL replay gaps across seek
    
    * Fix subscription replay across transient WAL rotation visibility
    
    * Hide rotated WAL under a non-WAL filename in regression test
    
    * Fix subscription WAL gap recovery behavior
    
    * Preserve absent local index in subscription WAL replay
    
    * Remove speculative WAL gap recovery from subscription fix
    
    * Remove unproven seek boundary changes from subscription fix
    
    * test: cover replicated WAL index after fragmented local requests
---
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |  7 +-
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |  6 +-
 .../consensus/ConsensusPrefetchingQueue.java       |  2 +-
 .../broker/consensus/ProgressWALIterator.java      |  4 +-
 .../broker/consensus/ProgressWALIteratorTest.java  | 76 +++++++++++++++++++++-
 5 files changed, 83 insertions(+), 12 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..5e92afb5f9c 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,9 @@ 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
+      
LOG_ARG_WAL_GAP_ARG_ARG_SKIPPED_ARG_TOTAL_SKIPPED_ARG_CAUSE_UNDETERMINED_D4E6491A
 =
+          "{}: WAL gap [{}, {}), skipped {}, total skipped {}; cause 
undetermined";
   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..14bc9cd4b26 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,9 @@ 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
+      
LOG_ARG_WAL_GAP_ARG_ARG_SKIPPED_ARG_TOTAL_SKIPPED_ARG_CAUSE_UNDETERMINED_D4E6491A
 =
+          "{}:WAL 缺口 [{}, {}),跳过 {} 条,累计跳过 {} 条;原因尚未确定";
   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 b692ec94ed0..8d2f0af7cec 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
@@ -2043,7 +2043,7 @@ public class ConsensusPrefetchingQueue {
       final long totalSkippedEntries = 
walGapSkippedEntries.addAndGet(skippedEntries);
       LOGGER.warn(
           DataNodePipeMessages
-              
.PIPE_LOG_CONSENSUSPREFETCHINGQUEUE_WAL_REPLAY_SKIPPED_UNAVAILABLE_SEARCH_INDEXES_B8023B64,
+              
.LOG_ARG_WAL_GAP_ARG_ARG_SKIPPED_ARG_TOTAL_SKIPPED_ARG_CAUSE_UNDETERMINED_D4E6491A,
           this,
           expectedSearchIndex,
           actualSearchIndex,
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
index ee61c105c15..6572473cb58 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIterator.java
@@ -734,7 +734,9 @@ public class ProgressWALIterator implements Closeable, 
Iterator<IndexedConsensus
     }
     buffer.position(SEARCH_INDEX_OFFSET);
     final long bodySearchIndex = 
SearchNode.extractSearchIndex(buffer.getLong());
-    return bodySearchIndex >= 0 ? bodySearchIndex : metadataSearchIndex;
+    // A replicated request has no local search index. The metadata fallback 
is based on the
+    // WAL entry offset, which also counts fragments and cannot assign a local 
index to it.
+    return bodySearchIndex;
   }
 
   private void startPending(
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
index 45bff39d6ba..c800a2327e9 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ProgressWALIteratorTest.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.db.subscription.broker.consensus;
 import 
org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeType;
 import org.apache.iotdb.consensus.common.request.IndexedConsensusRequest;
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.SearchNode;
 import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntryType;
 import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALInfoEntry;
 import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFileVersion;
@@ -87,7 +88,7 @@ public class ProgressWALIteratorTest {
   }
 
   @Test
-  public void testIteratorUsesMetadataSearchIndexForStartFiltering() throws 
Exception {
+  public void testIteratorUsesBodySearchIndexForStartFiltering() throws 
Exception {
     final Path dir = 
Files.createTempDirectory("progress-wal-iterator-metadata-search-index");
     final File firstWal =
         dir.resolve(WALFileUtils.getLogFileName(0, 0, 
WALFileStatus.CONTAINS_SEARCH_INDEX))
@@ -98,8 +99,8 @@ public class ProgressWALIteratorTest {
 
     try {
       try (WALWriter writer = new WALWriter(firstWal, WALFileVersion.V3)) {
-        writer.write(searchableEntry(-1L), singleEntryMeta(19, 5L, 1L, 1000L, 
7, 105L));
-        writer.write(searchableEntry(-1L), singleEntryMeta(19, 6L, 1L, 2000L, 
7, 106L));
+        writer.write(searchableEntry(5L), singleEntryMeta(19, 5L, 1L, 1000L, 
7, 105L));
+        writer.write(searchableEntry(6L), singleEntryMeta(19, 6L, 1L, 2000L, 
7, 106L));
       }
       try (WALWriter ignored = new WALWriter(lastWal, WALFileVersion.V3)) {
         // Create a sealed successor so the first WAL becomes historical and 
readable.
@@ -121,6 +122,75 @@ public class ProgressWALIteratorTest {
     }
   }
 
+  @Test
+  public void testIteratorPreservesAbsentLocalIndexAfterManyFragments() throws 
Exception {
+    final Path dir = 
Files.createTempDirectory("progress-wal-iterator-mixed-fragments");
+    final File dataWal =
+        dir.resolve(WALFileUtils.getLogFileName(0, 0, 
WALFileStatus.CONTAINS_SEARCH_INDEX))
+            .toFile();
+    final File successorWal =
+        dir.resolve(WALFileUtils.getLogFileName(1, 1878, 
WALFileStatus.CONTAINS_SEARCH_INDEX))
+            .toFile();
+
+    try {
+      try (WALWriter writer = new WALWriter(dataWal, WALFileVersion.V3)) {
+        // Physical WAL entries count fragments, not local consensus requests. 
The old fallback
+        // assigns the replicated entry 1877 + 550 = 2427, creating a false 
[1878, 2427) gap.
+        for (int fragment = 0; fragment < 550; fragment++) {
+          writer.write(searchableEntry(1877L), singleEntryMeta(19, 1877L, 1L, 
1000L, 7, 1877L));
+        }
+        writer.write(
+            searchableEntry(SearchNode.NO_CONSENSUS_INDEX),
+            singleEntryMeta(19, SearchNode.NO_CONSENSUS_INDEX, 1L, 2000L, 8, 
10000L));
+        writer.write(searchableEntry(1878L), singleEntryMeta(19, 1878L, 1L, 
3000L, 7, 1878L));
+      }
+      try (WALWriter ignored = new WALWriter(successorWal, WALFileVersion.V3)) 
{
+        // Keep both files retained and sealed; there is no deletion or 
concurrent write.
+      }
+
+      try (ProgressWALIterator iterator = new 
ProgressWALIterator(dir.toFile(), Long.MIN_VALUE)) {
+        assertTrue(iterator.hasNext());
+        final IndexedConsensusRequest fragmented = iterator.next();
+        assertEquals(1877L, fragmented.getSearchIndex());
+        assertEquals(1877L, fragmented.getProgressLocalSeq());
+        assertEquals(7, fragmented.getNodeId());
+        assertEquals(550, fragmented.getRequests().size());
+
+        assertTrue(iterator.hasNext());
+        final IndexedConsensusRequest replicated = iterator.next();
+        assertEquals(SearchNode.NO_CONSENSUS_INDEX, 
replicated.getSearchIndex());
+        assertEquals(10000L, replicated.getProgressLocalSeq());
+        assertEquals(2000L, replicated.getPhysicalTime());
+        assertEquals(8, replicated.getNodeId());
+        assertEquals(1, replicated.getRequests().size());
+
+        assertTrue(iterator.hasNext());
+        final IndexedConsensusRequest nextLocal = iterator.next();
+        assertEquals(1878L, nextLocal.getSearchIndex());
+        assertEquals(1878L, nextLocal.getProgressLocalSeq());
+        assertEquals(7, nextLocal.getNodeId());
+        assertEquals(1, nextLocal.getRequests().size());
+        assertFalse(iterator.hasNext());
+        assertFalse(iterator.hasIncompleteScan());
+        assertTrue(dataWal.isFile());
+      }
+
+      // A local-index seek must also retain replicated requests with their 
own writer progress.
+      try (ProgressWALIterator iterator = new 
ProgressWALIterator(dir.toFile(), 1878L)) {
+        assertTrue(iterator.hasNext());
+        assertEquals(SearchNode.NO_CONSENSUS_INDEX, 
iterator.next().getSearchIndex());
+        assertTrue(iterator.hasNext());
+        assertEquals(1878L, iterator.next().getSearchIndex());
+        assertFalse(iterator.hasNext());
+        assertFalse(iterator.hasIncompleteScan());
+      }
+    } finally {
+      Files.deleteIfExists(dataWal.toPath());
+      Files.deleteIfExists(successorWal.toPath());
+      Files.deleteIfExists(dir);
+    }
+  }
+
   @Test
   public void testIteratorMergesFragmentsWithSameLocalSeq() throws Exception {
     final Path dir = Files.createTempDirectory("progress-wal-iterator-merge");

Reply via email to