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 510fc7c4544 Fix subscription initialization retries for transient WAL 
reads (#18829)
510fc7c4544 is described below

commit 510fc7c45449c294f25fb2d7378de1518c599dc0
Author: Caideyipi <[email protected]>
AuthorDate: Wed Oct 14 11:06:25 2026 +0800

    Fix subscription initialization retries for transient WAL reads (#18829)
---
 .../consensus/ConsensusPrefetchingQueue.java       |  26 ++-
 ...onsensusPrefetchingQueueInitializationTest.java | 251 +++++++++++++++++++++
 2 files changed, 274 insertions(+), 3 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 dd5d2040009..44d9c101443 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
@@ -378,6 +378,7 @@ public class ConsensusPrefetchingQueue {
   protected enum ReplayLocateStatus {
     FOUND,
     AT_END,
+    RETRY_LATER,
     LOCATE_MISS
   }
 
@@ -433,6 +434,12 @@ public class ConsensusPrefetchingQueue {
           ReplayLocateStatus.LOCATE_MISS, Long.MIN_VALUE, 
recoveryRegionProgress, detail);
     }
 
+    static ReplayLocateDecision retryLater(
+        final RegionProgress recoveryRegionProgress, final String detail) {
+      return new ReplayLocateDecision(
+          ReplayLocateStatus.RETRY_LATER, Long.MIN_VALUE, 
recoveryRegionProgress, detail);
+    }
+
     protected ReplayLocateStatus getStatus() {
       return status;
     }
@@ -941,6 +948,11 @@ public class ConsensusPrefetchingQueue {
             recoveryRegionProgress,
             progressSource,
             configNodeProgressAvailable || hasPersistedState);
+    if (resolvedStart.getStatus() == ReplayLocateStatus.RETRY_LATER) {
+      // A rolling or temporarily unreadable WAL must leave initialization 
dormant. The next poll
+      // repeats the lookup before installing recovery frontiers or advancing 
the replay cursor.
+      return false;
+    }
 
     clearRecoveryWriterProgress();
     final RegionProgress effectiveRecoveryRegionProgress =
@@ -1008,6 +1020,8 @@ public class ConsensusPrefetchingQueue {
             resolvedReplayStartSearchIndex,
             replayTarget.getRecoveryRegionProgress(),
             progressSource + " (" + replayTarget.getDetail() + ")");
+      case RETRY_LATER:
+        return replayTarget;
       case LOCATE_MISS:
       default:
         throw new IllegalStateException(
@@ -1245,9 +1259,13 @@ public class ConsensusPrefetchingQueue {
       final ReplayLocateDecision decision =
           scanReplayStartForRequests(iterator, regionProgress, seekAfter);
       if (iterator.hasIncompleteScan()) {
-        return ReplayLocateDecision.locateMiss(
-            regionProgress,
-            "replay lookup did not complete: " + 
iterator.getIncompleteScanDetail());
+        final String detail =
+            "replay lookup did not complete: " + 
iterator.getIncompleteScanDetail();
+        // Retry only a deferred read. Confirmed corruption encountered 
earlier in the scan must
+        // still surface as a lookup failure, even if a later file is 
temporarily unreadable.
+        return iterator.isWaitingForReadableWal() && 
!iterator.hasSkippedBrokenWalFiles()
+            ? ReplayLocateDecision.retryLater(regionProgress, detail)
+            : ReplayLocateDecision.locateMiss(regionProgress, detail);
       }
       return decision;
     } catch (final IOException e) {
@@ -3402,6 +3420,7 @@ public class ConsensusPrefetchingQueue {
             replayTarget.getRecoveryRegionProgress(),
             "regionProgress");
         return;
+      case RETRY_LATER:
       case LOCATE_MISS:
       default:
         throw new IllegalStateException(
@@ -3443,6 +3462,7 @@ public class ConsensusPrefetchingQueue {
             replayTarget.getRecoveryRegionProgress(),
             "regionProgressAfter");
         return;
+      case RETRY_LATER:
       case LOCATE_MISS:
       default:
         throw new IllegalStateException(
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueInitializationTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueInitializationTest.java
new file mode 100644
index 00000000000..93b805c3032
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueInitializationTest.java
@@ -0,0 +1,251 @@
+/*
+ * 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.consensus.DataRegionId;
+import org.apache.iotdb.consensus.iot.IoTConsensusServerImpl;
+import org.apache.iotdb.consensus.iot.SubscriptionWalRetentionPolicy;
+import org.apache.iotdb.consensus.iot.WriterSafeFrontierTracker;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertNode;
+import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowNode;
+import org.apache.iotdb.db.queryengine.plan.statement.StatementTestUtils;
+import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALInfoEntry;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFileVersion;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALMetaData;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALWriter;
+import org.apache.iotdb.db.storageengine.dataregion.wal.node.WALNode;
+import 
org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALByteBufferForTest;
+import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileStatus;
+import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileUtils;
+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.WriterId;
+import org.apache.iotdb.rpc.subscription.payload.poll.WriterProgress;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.ArgumentCaptor;
+
+import java.io.File;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ConsensusPrefetchingQueueInitializationTest {
+
+  @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  private final DataRegionId regionId = new DataRegionId(3);
+  private final RegionProgress committedProgress =
+      new RegionProgress(
+          Collections.singletonMap(
+              new WriterId(regionId.toString(), 5), new WriterProgress(1000L, 
1L)));
+  private final WALNode walNode = mock(WALNode.class);
+  private final ConsensusLogToTabletConverter converter = 
mock(ConsensusLogToTabletConverter.class);
+  private String originalSystemDir;
+  private File walDirectory;
+  private ConsensusPrefetchingQueue queue;
+
+  @Before
+  public void setUp() throws Exception {
+    originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    IoTDBDescriptor.getInstance()
+        .getConfig()
+        .setSystemDir(temporaryFolder.newFolder("system").getAbsolutePath());
+    walDirectory = temporaryFolder.newFolder("wal");
+    when(walNode.getLogDirectory()).thenReturn(walDirectory);
+    when(walNode.getCurrentSearchIndex()).thenReturn(3L);
+    when(walNode.getCurrentWALFileVersion()).thenReturn(3L);
+    when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(new 
WALMetaData());
+    when(converter.convert(any())).thenReturn(Collections.emptyList());
+    when(converter.isTableModel()).thenReturn(true);
+
+    final IoTConsensusServerImpl server = mock(IoTConsensusServerImpl.class);
+    when(server.getConsensusReqReader()).thenReturn(walNode);
+    when(server.getWriterSafeFrontierTracker()).thenReturn(new 
WriterSafeFrontierTracker());
+    final ConsensusSubscriptionCommitManager commitManager =
+        new ConsensusSubscriptionCommitManager(
+            (consumerGroupId, topicName, ignored) ->
+                
ConsensusSubscriptionCommitManager.ConfigNodeProgressQueryResult.available(
+                    committedProgress));
+    queue =
+        new ConsensusPrefetchingQueue(
+            "consumerGroup",
+            "topic",
+            TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+            regionId,
+            server,
+            new SubscriptionWalRetentionPolicy(
+                "topic",
+                SubscriptionWalRetentionPolicy.UNBOUNDED,
+                SubscriptionWalRetentionPolicy.UNBOUNDED),
+            converter,
+            commitManager,
+            committedProgress,
+            4L,
+            1L,
+            true);
+  }
+
+  @After
+  public void tearDown() {
+    try {
+      if (queue != null) {
+        queue.close();
+      }
+    } finally {
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
+  @Test
+  public void testInitializationRetriesFileDisappearingDuringLookup() throws 
Exception {
+    final File dataWal = walFile(1L);
+    writeRequests(dataWal);
+    try (WALWriter ignored = new WALWriter(walFile(3L), WALFileVersion.V3)) {
+      // Keep the data file sealed, with a live successor in the directory 
listing.
+    }
+    final File hiddenWal = new File(temporaryFolder.getRoot(), "hidden.wal");
+    final AtomicBoolean hideFile = new AtomicBoolean(true);
+    when(walNode.getCurrentWALFileVersion())
+        .thenAnswer(
+            invocation -> {
+              // The iterator has listed the old filename but has not opened 
it yet.
+              if (hideFile.compareAndSet(true, false)) {
+                Files.move(dataWal.toPath(), hiddenWal.toPath());
+              }
+              return 3L;
+            });
+
+    assertInitializationDeferred();
+    Files.move(hiddenWal.toPath(), dataWal.toPath());
+    assertResumesAfterCommittedProgress();
+  }
+
+  @Test
+  public void testRepeatedUnreadablePollsKeepInitializationDormant() throws 
Exception {
+    final File liveWal = walFile(3L);
+    assertTrue(liveWal.mkdir());
+    for (int attempt = 0; attempt < 3; attempt++) {
+      assertInitializationDeferred();
+      queue.drivePrefetchOnce();
+      verify(converter, never()).convert(any());
+    }
+
+    Files.delete(liveWal.toPath());
+    final WALMetaData metadata = writeRequests(liveWal);
+    when(walNode.getCurrentWALMetaDataSnapshot()).thenReturn(metadata);
+    assertResumesAfterCommittedProgress();
+  }
+
+  @Test
+  public void testConfirmedRetainedCorruptionStillFailsInitialization() throws 
Exception {
+    Files.write(walFile(1L).toPath(), new byte[128]);
+    try (WALWriter ignored = new WALWriter(walFile(3L), WALFileVersion.V3)) {
+      // Version 1 is retained history, outside the near-live retry window.
+    }
+
+    assertThrows(IllegalStateException.class, () -> queue.poll("consumer"));
+    assertEquals(0L, queue.getInitializedStatus());
+    assertEquals(4L, queue.getCurrentReadSearchIndex());
+    verify(converter, never()).convert(any());
+  }
+
+  @Test
+  public void testUnreadableLiveFileDoesNotMaskRetainedCorruption() throws 
Exception {
+    Files.write(walFile(1L).toPath(), new byte[128]);
+    assertTrue(walFile(3L).mkdir());
+
+    assertThrows(IllegalStateException.class, () -> queue.poll("consumer"));
+    assertEquals(0L, queue.getInitializedStatus());
+    assertEquals(4L, queue.getCurrentReadSearchIndex());
+    verify(converter, never()).convert(any());
+  }
+
+  private void assertInitializationDeferred() throws Exception {
+    assertNull(queue.poll("consumer"));
+    assertEquals(0L, queue.getInitializedStatus());
+    assertEquals(4L, queue.getCurrentReadSearchIndex());
+    assertEquals(0L, queue.getWalGapSkippedEntries());
+    assertEquals(0L, queue.getUnreadableWalFileCount());
+    assertEquals(0L, queue.getUnreadableWalEntryCount());
+    verify(converter, never()).convert(any());
+  }
+
+  private void assertResumesAfterCommittedProgress() throws Exception {
+    assertNull(queue.poll("consumer"));
+    assertEquals(1L, queue.getInitializedStatus());
+    assertEquals(2L, queue.getCurrentReadSearchIndex());
+    queue.drivePrefetchOnce();
+    final ArgumentCaptor<InsertNode> requests = 
ArgumentCaptor.forClass(InsertNode.class);
+    verify(converter, times(2)).convert(requests.capture());
+    assertEquals(
+        Arrays.asList(2L, 3L),
+        Arrays.asList(
+            requests.getAllValues().get(0).getSearchIndex(),
+            requests.getAllValues().get(1).getSearchIndex()));
+    assertEquals(4L, queue.getCurrentReadSearchIndex());
+    assertEquals(0L, queue.getWalGapSkippedEntries());
+    assertEquals(0L, queue.getUnreadableWalFileCount());
+    assertEquals(0L, queue.getUnreadableWalEntryCount());
+  }
+
+  private File walFile(final long versionId) {
+    return new File(
+        walDirectory,
+        WALFileUtils.getLogFileName(versionId, 1L, 
WALFileStatus.CONTAINS_SEARCH_INDEX));
+  }
+
+  private static WALMetaData writeRequests(final File walFile) throws 
Exception {
+    final WALMetaData metadata = new WALMetaData();
+    try (WALWriter writer = new WALWriter(walFile, WALFileVersion.V3)) {
+      for (long searchIndex = 1L; searchIndex <= 3L; searchIndex++) {
+        final RelationalInsertRowNode node =
+            StatementTestUtils.genInsertRowNode(Math.toIntExact(searchIndex));
+        node.setSearchIndex(searchIndex);
+        final WALInfoEntry entry = new WALInfoEntry(1L, node);
+        final ByteBuffer buffer = ByteBuffer.allocate(entry.serializedSize());
+        entry.serialize(new WALByteBufferForTest(buffer));
+        final WALMetaData entryMetadata = new WALMetaData();
+        entryMetadata.add(entry.serializedSize(), searchIndex, 1L, 1000L, 5, 
searchIndex);
+        writer.write(buffer, entryMetadata);
+        metadata.addAll(entryMetadata);
+      }
+    }
+    return metadata;
+  }
+}

Reply via email to