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 4f86b5681ea [Subscription] Diagnose stalled replay and WAL footer 
recovery (#18970)
4f86b5681ea is described below

commit 4f86b5681ea075ed4c79287cb572198224cf4ecd
Author: Caideyipi <[email protected]>
AuthorDate: Thu Oct 22 12:32:46 2026 +0800

    [Subscription] Diagnose stalled replay and WAL footer recovery (#18970)
---
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |   4 +-
 .../iotdb/db/i18n/StorageEngineMessages.java       |   3 +
 .../apache/iotdb/db/i18n/DataNodePipeMessages.java |   4 +-
 .../iotdb/db/i18n/StorageEngineMessages.java       |   3 +
 .../dataregion/wal/io/WALMetaData.java             |  36 +++++
 .../dataregion/wal/recover/WALRepairWriter.java    |  15 +++
 .../consensus/ConsensusPrefetchingQueue.java       | 145 ++++++++++++++++++++-
 .../broker/consensus/ProgressWALIterator.java      |  22 ++++
 .../wal/recover/WALRepairWriterTest.java           |  65 +++++++++
 .../consensus/ConsensusPrefetchingQueueTest.java   |  24 +++-
 ...onsensusPrefetchingQueueWriterOrderingTest.java |  87 +++++++++++++
 11 files changed, 399 insertions(+), 9 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 bb04beac563..8eb2b8a6116 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
@@ -20,8 +20,8 @@
 package org.apache.iotdb.db.i18n;
 
 public final class DataNodePipeMessages {
-  public static final String 
LOG_SUBSCRIPTION_REPLAY_HAS_MADE_NO_CURSOR_PROGRESS_FOR_ARG_MS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_FAE1F64B
 =
-      "Subscription replay has made no cursor progress for {} ms: {}. Inspect 
prefetch worker stacks and WAL I/O.";
+  public static final String 
LOG_SUBSCRIPTION_REPLAY_STALLED_FOR_ARG_MS_ARG_DIAGNOSTICS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_CB012F98
 =
+      "Subscription replay stalled for {} ms: {}; diagnostics={}. Inspect 
prefetch worker stacks and WAL I/O.";
 
   public static final String
       
LOG_SUBSCRIPTION_PREFETCH_ROUND_STARTING_FOR_ARG_ACTIVE_ARG_INITIALIZED_ARG_CLOSED_ARG_PENDINGENTRIES_ARG_PREFETCHEDEVENTS_ARG_INFLIGHTEVENTS_ARG_NEXTREADSEARCHINDEX_ARG_F75FF2CD
 =
diff --git 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/StorageEngineMessages.java
 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/StorageEngineMessages.java
index 1adfd8ac3cc..294af1dbb28 100644
--- 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/StorageEngineMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/StorageEngineMessages.java
@@ -21,6 +21,9 @@ package org.apache.iotdb.db.i18n;
 
 public final class StorageEngineMessages {
 
+  public static final String 
LOG_REBUILT_UNREADABLE_WAL_FOOTER_FILE_ARG_VERSION_ARG_ORIGINALFILEBYTES_ARG_WRITERPROGRESS_ARG_1DBF836E
 =
+      "Rebuilt unreadable WAL footer: file={}, version={}, 
originalFileBytes={}, writerProgress={}.";
+
   public static final String 
MESSAGE_LOAD_TSFILE_SLICE_BODY_IS_NULL_OR_EMPTY_2A65366C =
       "Load TsFile slice body is null or empty";
   public static final String 
MESSAGE_INVALID_LOAD_TSFILE_SLICE_METADATA_SLICECOUNT_ARG_ORIGINBODYSIZE_ARG_379BF1B8
 =
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 d319d0e3772..ee86a424c29 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
@@ -20,8 +20,8 @@
 package org.apache.iotdb.db.i18n;
 
 public final class DataNodePipeMessages {
-  public static final String 
LOG_SUBSCRIPTION_REPLAY_HAS_MADE_NO_CURSOR_PROGRESS_FOR_ARG_MS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_FAE1F64B
 =
-      "订阅回放游标已连续 {} ms 未推进:{}。请检查预取工作线程栈和 WAL I/O。";
+  public static final String 
LOG_SUBSCRIPTION_REPLAY_STALLED_FOR_ARG_MS_ARG_DIAGNOSTICS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_CB012F98
 =
+      "订阅回放已停滞 {} ms:{};diagnostics={}。请检查预取工作线程栈和 WAL I/O。";
 
   public static final String
       
LOG_SUBSCRIPTION_PREFETCH_ROUND_STARTING_FOR_ARG_ACTIVE_ARG_INITIALIZED_ARG_CLOSED_ARG_PENDINGENTRIES_ARG_PREFETCHEDEVENTS_ARG_INFLIGHTEVENTS_ARG_NEXTREADSEARCHINDEX_ARG_F75FF2CD
 =
diff --git 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/StorageEngineMessages.java
 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/StorageEngineMessages.java
index 0a0d3b266a4..49f0e760e0a 100644
--- 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/StorageEngineMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/StorageEngineMessages.java
@@ -21,6 +21,9 @@ package org.apache.iotdb.db.i18n;
 
 public final class StorageEngineMessages {
 
+  public static final String 
LOG_REBUILT_UNREADABLE_WAL_FOOTER_FILE_ARG_VERSION_ARG_ORIGINALFILEBYTES_ARG_WRITERPROGRESS_ARG_1DBF836E
 =
+      "已重建不可读的 WAL 
footer:file={},version={},originalFileBytes={},writerProgress={}。";
+
   public static final String 
MESSAGE_LOAD_TSFILE_SLICE_BODY_IS_NULL_OR_EMPTY_2A65366C =
       "Load TsFile 分片内容为 null 或为空";
   public static final String 
MESSAGE_INVALID_LOAD_TSFILE_SLICE_METADATA_SLICECOUNT_ARG_ORIGINBODYSIZE_ARG_379BF1B8
 =
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALMetaData.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALMetaData.java
index 50ae0961ce1..5d316a7508f 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALMetaData.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/io/WALMetaData.java
@@ -39,6 +39,7 @@ import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -312,6 +313,41 @@ public class WALMetaData implements SerializedSize {
     return localSeqs;
   }
 
+  /** Diagnostic summary for an abnormal footer recovery, never for the normal 
write path. */
+  public Map<String, Object> getWriterProgressDiagnostics() {
+    final Map<String, Object> result = new LinkedHashMap<>();
+    final int entries = buffersSize.size();
+    final int progressEntries =
+        Math.min(
+            entries, Math.min(physicalTimes.size(), Math.min(nodeIds.size(), 
localSeqs.size())));
+    int attributedEntries = 0;
+    int defaultLocalProgressEntries = 0;
+    final List<String> unattributedSamples = new ArrayList<>();
+    for (int i = 0; i < progressEntries; i++) {
+      final long physicalTime = physicalTimes.get(i);
+      final int nodeId = nodeIds.get(i);
+      final long localSeq = localSeqs.get(i);
+      if (nodeId >= 0 && physicalTime >= 0 && localSeq >= 0) {
+        attributedEntries++;
+      } else {
+        if (unattributedSamples.size() < 4) {
+          unattributedSamples.add(i + ":(" + physicalTime + "," + nodeId + "," 
+ localSeq + ")");
+        }
+        if (nodeId < 0 && physicalTime == 0 && localSeq >= 0) {
+          defaultLocalProgressEntries++;
+        }
+      }
+    }
+    result.put("entryCount", entries);
+    result.put("firstSearchIndex", firstSearchIndex);
+    result.put("attributedProgressEntries", attributedEntries);
+    result.put("unattributedProgressEntries", progressEntries - 
attributedEntries);
+    result.put("missingProgressEntries", entries - progressEntries);
+    result.put("defaultLocalProgressEntries", defaultLocalProgressEntries);
+    result.put("unattributedSamples", unattributedSamples);
+    return result;
+  }
+
   private int getNodeIdOverrideCount() {
     final short defaultNodeId = computeDefaultNodeId();
     int count = 0;
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRepairWriter.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRepairWriter.java
index 94129d9e220..7572eac68cb 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRepairWriter.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRepairWriter.java
@@ -19,12 +19,16 @@
 
 package org.apache.iotdb.db.storageengine.dataregion.wal.recover;
 
+import org.apache.iotdb.db.i18n.StorageEngineMessages;
 import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntryType;
 import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALByteBufReader;
 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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.io.File;
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -38,6 +42,7 @@ import java.nio.file.StandardOpenOption;
 
 /** Check whether the wal file is broken and repair it. */
 public class WALRepairWriter {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(WALRepairWriter.class);
   private final File logFile;
 
   public WALRepairWriter(File logFile) {
@@ -63,6 +68,7 @@ public class WALRepairWriter {
       quarantine();
       return false;
     }
+    final long originalFileBytes = logFile.length();
     // A channel offset may include a partially read entry in the same 
compressed segment. Rebuild
     // complete entries in a temporary file instead of truncating at a 
read-ahead offset. Publish
     // only after every entry and the new footer have been written and forced 
successfully.
@@ -111,6 +117,15 @@ public class WALRepairWriter {
     } finally {
       Files.deleteIfExists(repaired);
     }
+    if (LOGGER.isWarnEnabled()) {
+      LOGGER.warn(
+          StorageEngineMessages
+              
.LOG_REBUILT_UNREADABLE_WAL_FOOTER_FILE_ARG_VERSION_ARG_ORIGINALFILEBYTES_ARG_WRITERPROGRESS_ARG_1DBF836E,
+          logFile,
+          version,
+          originalFileBytes,
+          metaData.getWriterProgressDiagnostics());
+    }
     return true;
   }
 
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 24366687030..df756dc2d22 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
@@ -351,6 +351,8 @@ public class ConsensusPrefetchingQueue {
   private long lastReplayProgressNanos;
   private boolean replayStallWarningLogged;
   private long lastReplayStallWarningNanos;
+  private WriterFrontier lastBlockedWriterFrontier;
+  private boolean lastWriterBarrierInOrderedReplay;
 
   private volatile String lastConsumerId = "";
 
@@ -1687,6 +1689,7 @@ public class ConsensusPrefetchingQueue {
   private static final long PREFETCH_STATS_LOG_INTERVAL_MS = 5_000L;
   private static final long REPLAY_STALL_WARNING_THRESHOLD_NANOS = 
TimeUnit.SECONDS.toNanos(60L);
   private static final long REPLAY_STALL_WARNING_INTERVAL_NANOS = 
TimeUnit.SECONDS.toNanos(30L);
+  private static final int MAX_REPLAY_DIAGNOSTIC_WRITERS = 4;
 
   private static final long SLOW_PREFETCH_ROUND_THRESHOLD_MS = 10_000L;
 
@@ -1800,6 +1803,8 @@ public class ConsensusPrefetchingQueue {
             getPrefetchDurationMs());
       }
       logPeriodicStatsIfNecessary();
+      // Preserve the previous round's actual ordering decision until the 
stall reporter sees it.
+      lastBlockedWriterFrontier = null;
 
       final long currentSeekGeneration = seekGeneration.get();
       if (currentSeekGeneration != observedSeekGeneration) {
@@ -2067,6 +2072,8 @@ public class ConsensusPrefetchingQueue {
       }
       final WriterFrontier next = frontiers.peek();
       if (Objects.isNull(next) || next.isBarrier) {
+        lastBlockedWriterFrontier = next;
+        lastWriterBarrierInOrderedReplay = true;
         break;
       }
       final PreparedEntry buffered = peekRealtimeEntry(next.writerNodeId);
@@ -2393,19 +2400,138 @@ public class ConsensusPrefetchingQueue {
     final long stalledNanos = nowNanos - lastReplayProgressNanos;
     if (stalledNanos >= REPLAY_STALL_WARNING_THRESHOLD_NANOS
         && (!replayStallWarningLogged
-            || nowNanos - lastReplayStallWarningNanos >= 
REPLAY_STALL_WARNING_INTERVAL_NANOS)) {
+            || nowNanos - lastReplayStallWarningNanos >= 
REPLAY_STALL_WARNING_INTERVAL_NANOS)
+        && LOGGER.isWarnEnabled()) {
       LOGGER.warn(
           DataNodePipeMessages
-              
.LOG_SUBSCRIPTION_REPLAY_HAS_MADE_NO_CURSOR_PROGRESS_FOR_ARG_MS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_FAE1F64B,
+              
.LOG_SUBSCRIPTION_REPLAY_STALLED_FOR_ARG_MS_ARG_DIAGNOSTICS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_CB012F98,
           TimeUnit.NANOSECONDS.toMillis(stalledNanos),
-          this);
+          this,
+          getReplayStallDiagnostics());
       lastReplayStallWarningNanos = nowNanos;
       replayStallWarningLogged = true;
     }
   }
 
+  private Map<String, Object> getReplayStallDiagnostics() {
+    // Only called for the throttled stall warning, on the serialized prefetch 
worker. Do not
+    // refresh frontiers, probe WAL readers, or dump requests/Tablets while 
taking this snapshot.
+    final Map<String, Object> result = new LinkedHashMap<>();
+    result.put("runtimeVersion", runtimeVersion);
+    result.put("admissionBlockReason", realtimeAdmissionBlockReason);
+    result.put("memoryBlockReason", memoryBlockReason);
+    result.put("prefetchLimitReached", hasReachedPrefetchLimit());
+    result.put("lingerEntryCount", lingerBatch.entryCount);
+    result.put("lastBlockedFrontier", lastBlockedWriterFrontier);
+    result.put(
+        "lastBarrierInOrderedReplay",
+        Objects.nonNull(lastBlockedWriterFrontier) ? 
lastWriterBarrierInOrderedReplay : null);
+    final Map<Integer, SafeFrontier> safeFrontiers =
+        serverImpl.getWriterSafeFrontierTracker().snapshotSafeFrontiers();
+    result.put("activeWriterCount", activeWriterNodeIds.size());
+    result.put("activeWriters", sampleWriterDiagnostics(activeWriterNodeIds, 
safeFrontiers));
+    result.put("realtimeWriterCount", realtimeEntriesByWriter.size());
+    result.put(
+        "realtimeWriters",
+        sampleWriterDiagnostics(realtimeEntriesByWriter.keySet(), 
safeFrontiers));
+    result.put("orderedWriterCount", orderedWriterReplay.size());
+    result.put(
+        "orderedWriters", 
sampleWriterDiagnostics(orderedWriterReplay.keySet(), safeFrontiers));
+    final RegionProgress committed =
+        commitManager.getCommittedRegionProgress(consumerGroupId, topicName, 
consensusGroupId);
+    result.put("committedWriterCount", committed.getWriterPositions().size());
+    final List<String> committedSamples = new ArrayList<>();
+    for (final Map.Entry<WriterId, WriterProgress> entry :
+        committed.getWriterPositions().entrySet()) {
+      if (committedSamples.size() >= MAX_REPLAY_DIAGNOSTIC_WRITERS) {
+        break;
+      }
+      committedSamples.add(entry.toString());
+    }
+    result.put("committedWriters", committedSamples);
+    result.put("committedRetainedMinVersionId", committedRetainedMinVersionId);
+    final WalFileCommitRequirement requirement =
+        walFileCommitRequirements.get(committedRetainedMinVersionId);
+    result.put(
+        "retentionContainsUnsupportedProgress",
+        Objects.nonNull(requirement) ? requirement.containsUnsupportedProgress 
: null);
+    if (consensusReqReader instanceof WALNode) {
+      result.put("walDirectory", ((WALNode) 
consensusReqReader).getLogDirectory());
+    }
+    result.put(
+        "walScanState",
+        Objects.nonNull(subscriptionWALIterator)
+            ? subscriptionWALIterator.getDiagnosticState()
+            : null);
+    return result;
+  }
+
+  private List<Map<String, Object>> sampleWriterDiagnostics(
+      final Set<Integer> writerNodeIds, final Map<Integer, SafeFrontier> 
safeFrontiers) {
+    final List<Map<String, Object>> samples = new ArrayList<>();
+    final Set<Integer> sampledWriterNodeIds = new LinkedHashSet<>();
+    // Keep unknown progress and the actual blocker visible even when other 
writers fill the cap.
+    if (writerNodeIds.contains(-1)) {
+      sampledWriterNodeIds.add(-1);
+    }
+    if (Objects.nonNull(lastBlockedWriterFrontier)
+        && writerNodeIds.contains(lastBlockedWriterFrontier.writerNodeId)) {
+      sampledWriterNodeIds.add(lastBlockedWriterFrontier.writerNodeId);
+    }
+    for (final int writerNodeId : writerNodeIds) {
+      if (sampledWriterNodeIds.size() >= MAX_REPLAY_DIAGNOSTIC_WRITERS) {
+        break;
+      }
+      sampledWriterNodeIds.add(writerNodeId);
+    }
+    for (final int writerNodeId : sampledWriterNodeIds) {
+      final Map<String, Object> sample = new LinkedHashMap<>();
+      sample.put("writerNodeId", writerNodeId);
+      final WriterState state = writerStates.get(writerNodeId);
+      sample.put("effectiveSafePt", Objects.nonNull(state) ? 
state.effectiveSafePt : null);
+      sample.put("closed", Objects.nonNull(state) ? state.closed : null);
+      final SafeFrontier safeFrontier = safeFrontiers.get(writerNodeId);
+      if (Objects.nonNull(safeFrontier)) {
+        sample.put("trackerSafePt", safeFrontier.getPhysicalTime());
+        sample.put("trackerAppliedLocalSeq", safeFrontier.getLocalSeq());
+      }
+      final PreparedEntry buffered = peekRealtimeEntry(writerNodeId);
+      if (Objects.nonNull(buffered)) {
+        sample.put("bufferedHead", buffered.getOrderingKey());
+        sample.put("bufferedSearchIndex", buffered.searchIndex);
+        sample.put("bufferedEntryCount", 
realtimeEntriesByWriter.get(writerNodeId).size());
+      }
+      sample.put("materializedProgress", 
materializedProgressByWriter.get(writerNodeId));
+      sample.put(
+          "recoveryProgress",
+          recoveryWriterProgressByWriter.get(
+              new WriterId(consensusGroupId.toString(), writerNodeId)));
+      final WriterReplayState replay = orderedWriterReplay.get(writerNodeId);
+      if (Objects.nonNull(replay)) {
+        sample.put("consumedLocalSeq", replay.consumedLocalSeq);
+        sample.put("caughtUp", replay.caughtUp);
+        sample.put("fromPending", replay.fromPending);
+        if (Objects.nonNull(replay.head)) {
+          sample.put(
+              "rawHead",
+              new OrderingKey(
+                  replay.head.getPhysicalTime(),
+                  replay.head.getNodeId(),
+                  replay.head.getProgressLocalSeq()));
+          sample.put("rawSearchIndex", replay.head.getSearchIndex());
+        }
+        sample.put(
+            "walScanState",
+            Objects.nonNull(replay.iterator) ? 
replay.iterator.getDiagnosticState() : null);
+      }
+      samples.add(sample);
+    }
+    return samples;
+  }
+
   private void resetRoundStateForSeek(final long newSeekGeneration) {
     replayProgressObserved = false;
+    lastBlockedWriterFrontier = null;
     restorePendingSubscriptionWalCursor(newSeekGeneration);
     discardRealtimeEntries();
     discardBatch(lingerBatch);
@@ -3734,6 +3860,8 @@ public class ConsensusPrefetchingQueue {
       }
       final WriterFrontier frontier = frontiers.peek();
       if (Objects.isNull(frontier) || frontier.isBarrier) {
+        lastBlockedWriterFrontier = frontier;
+        lastWriterBarrierInOrderedReplay = false;
         return true;
       }
       final T writerHead = headSupplier.apply(frontier.writerNodeId);
@@ -6243,6 +6371,17 @@ public class ConsensusPrefetchingQueue {
           writerNodeId, new OrderingKey(effectiveSafePt, Integer.MIN_VALUE, 
Long.MIN_VALUE), true);
     }
 
+    @Override
+    public String toString() {
+      return "WriterFrontier{writerNodeId="
+          + writerNodeId
+          + ", orderingKey="
+          + orderingKey
+          + ", barrier="
+          + isBarrier
+          + '}';
+    }
+
     @Override
     public int compareTo(final WriterFrontier other) {
       int cmp = orderingKey.compareTo(other.orderingKey);
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 c048677a0af..cbb0d7099ec 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
@@ -527,6 +527,28 @@ public class ProgressWALIterator implements Closeable, 
Iterator<IndexedConsensus
         + "]";
   }
 
+  String getDiagnosticState() {
+    // Called only by the throttled queue stall warning; never read entry 
bodies or advance here.
+    return this
+        + "[file="
+        + currentReaderFile
+        + ", liveSnapshot="
+        + currentReaderUsesLiveSnapshot
+        + ", metadataRecoveredFromEntries="
+        + (currentReader != null && 
currentReader.getMetaData().isRecoveredFromEntries())
+        + ", pendingProgress=("
+        + pendingPhysicalTime
+        + ","
+        + pendingNodeId
+        + ","
+        + pendingLocalSeq
+        + "), pendingSearchIndex="
+        + pendingSearchIndex
+        + ", pendingFragments="
+        + pendingRequests.size()
+        + "]";
+  }
+
   String getSkippedBrokenWalFileDetails(final int firstFileIndex) {
     final int firstAvailableIndex =
         getSkippedBrokenWalFileCount() - skippedBrokenWalFileDetails.size();
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRepairWriterTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRepairWriterTest.java
index ea0a615d984..db91a92fece 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRepairWriterTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/wal/recover/WALRepairWriterTest.java
@@ -21,6 +21,7 @@ package 
org.apache.iotdb.db.storageengine.dataregion.wal.recover;
 import org.apache.iotdb.commons.exception.IllegalPathException;
 import org.apache.iotdb.commons.path.PartialPath;
 import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import org.apache.iotdb.db.i18n.StorageEngineMessages;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode;
 import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntry;
@@ -34,6 +35,10 @@ import 
org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileStatus;
 import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileUtils;
 import org.apache.iotdb.db.utils.constant.TestConstant;
 
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
 import org.apache.tsfile.common.conf.TSFileConfig;
 import org.apache.tsfile.enums.TSDataType;
 import org.apache.tsfile.utils.Binary;
@@ -42,6 +47,7 @@ import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
@@ -53,6 +59,8 @@ import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.stream.Stream;
 
 public class WALRepairWriterTest {
@@ -76,6 +84,63 @@ public class WALRepairWriterTest {
     }
   }
 
+  @Test
+  public void testFooterRepairReportsUnattributedProgressOnce() throws 
Exception {
+    final Logger logger = (Logger) 
LoggerFactory.getLogger(WALRepairWriter.class);
+    final Level originalLevel = logger.getLevel();
+    final ListAppender<ILoggingEvent> appender = new ListAppender<>();
+    logger.setLevel(Level.WARN);
+    appender.start();
+    logger.addAppender(appender);
+    try {
+      long dataEnd;
+      try (WALWriter writer = new WALWriter(logFile)) {
+        for (int i = 1; i <= 8; i++) {
+          final InsertRowNode node = getInsertRowNode();
+          // Include replicated entries whose local search index cannot 
identify their writer.
+          node.setSearchIndex(i == 8 ? -1 : i);
+          final WALInfoEntry entry = new WALInfoEntry(1, node);
+          final ByteBuffer buffer = 
ByteBuffer.allocate(entry.serializedSize());
+          entry.serialize(new WALByteBufferForTest(buffer));
+          final WALMetaData metadata = new WALMetaData();
+          metadata.add(entry.serializedSize(), node.getSearchIndex(), 1, 1000, 
i == 8 ? 8 : 7, i);
+          writer.write(buffer, metadata);
+        }
+        dataEnd = writer.getOffset();
+      }
+      try (FileChannel channel = FileChannel.open(logFile.toPath(), 
StandardOpenOption.WRITE)) {
+        channel.truncate(dataEnd);
+      }
+      final WALMetaData recovered;
+      try (FileChannel channel = FileChannel.open(logFile.toPath(), 
StandardOpenOption.READ)) {
+        recovered = WALMetaData.readFromWALFile(logFile, channel);
+      }
+      Assert.assertTrue(new WALRepairWriter(logFile).repair(recovered));
+      // A second check of the intact file must neither rescan diagnostic 
arrays nor log again.
+      Assert.assertTrue(new WALRepairWriter(logFile).repair(recovered));
+      Assert.assertEquals(1, appender.list.size());
+      final ILoggingEvent warning = appender.list.get(0);
+      Assert.assertEquals(
+          StorageEngineMessages
+              
.LOG_REBUILT_UNREADABLE_WAL_FOOTER_FILE_ARG_VERSION_ARG_ORIGINALFILEBYTES_ARG_WRITERPROGRESS_ARG_1DBF836E,
+          warning.getMessage());
+      final Map<?, ?> progress = (Map<?, ?>) warning.getArgumentArray()[3];
+      Assert.assertEquals(8, progress.get("entryCount"));
+      Assert.assertEquals(0, progress.get("attributedProgressEntries"));
+      Assert.assertEquals(8, progress.get("unattributedProgressEntries"));
+      Assert.assertEquals(7, progress.get("defaultLocalProgressEntries"));
+      Assert.assertEquals(4, ((List<?>) 
progress.get("unattributedSamples")).size());
+      
Assert.assertTrue(warning.getFormattedMessage().contains(logFile.toString()));
+      try (WALByteBufReader reader = new WALByteBufReader(logFile)) {
+        Assert.assertEquals(8, reader.getMetaData().getBuffersSize().size());
+      }
+    } finally {
+      logger.detachAppender(appender);
+      logger.setLevel(originalLevel);
+      appender.stop();
+    }
+  }
+
   @Test
   public void testEmptyFile() throws IOException {
     // prepare file
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 ba3c2a972ce..8d7edd979c5 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
@@ -88,6 +88,8 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
@@ -146,7 +148,7 @@ public class ConsensusPrefetchingQueueTest {
           new ConsensusPrefetchingQueue(
               "consumerGroup",
               "topic",
-              TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+              TopicConstant.ORDER_MODE_MULTI_WRITER_VALUE,
               new DataRegionId(1),
               server,
               new SubscriptionWalRetentionPolicy(
@@ -163,6 +165,7 @@ public class ConsensusPrefetchingQueueTest {
           
ConsensusPrefetchingQueue.class.getDeclaredField("prefetchInitialized");
       initialized.setAccessible(true);
       initialized.setBoolean(queue, true);
+      queue.setActiveWriterNodeIds(Set.of(1, 2, 3, 4, 5, 6, 7, 8));
       final Method report =
           ConsensusPrefetchingQueue.class.getDeclaredMethod(
               "reportReplayStallIfNecessary", long.class);
@@ -176,7 +179,7 @@ public class ConsensusPrefetchingQueueTest {
                               .getMessage()
                               .equals(
                                   DataNodePipeMessages
-                                      
.LOG_SUBSCRIPTION_REPLAY_HAS_MADE_NO_CURSOR_PROGRESS_FOR_ARG_MS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_FAE1F64B))
+                                      
.LOG_SUBSCRIPTION_REPLAY_STALLED_FOR_ARG_MS_ARG_DIAGNOSTICS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_CB012F98))
                   .count();
       report.invoke(queue, 0L);
       report.invoke(queue, TimeUnit.SECONDS.toNanos(59L));
@@ -184,6 +187,23 @@ public class ConsensusPrefetchingQueueTest {
       report.invoke(queue, TimeUnit.SECONDS.toNanos(60L));
       report.invoke(queue, TimeUnit.SECONDS.toNanos(89L));
       assertEquals(Long.valueOf(1L), warningCount.get());
+      final Map<?, ?> diagnostics =
+          (Map<?, ?>)
+              appender.list.stream()
+                  .filter(
+                      event ->
+                          event
+                              .getMessage()
+                              .equals(
+                                  DataNodePipeMessages
+                                      
.LOG_SUBSCRIPTION_REPLAY_STALLED_FOR_ARG_MS_ARG_DIAGNOSTICS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_CB012F98))
+                  .findFirst()
+                  .get()
+                  .getArgumentArray()[2];
+      assertEquals(8, diagnostics.get("activeWriterCount"));
+      assertEquals(4, ((List<?>) diagnostics.get("activeWriters")).size());
+      assertTrue(((List<?>) diagnostics.get("realtimeWriters")).isEmpty());
+      assertTrue(((List<?>) diagnostics.get("orderedWriters")).isEmpty());
       report.invoke(queue, TimeUnit.SECONDS.toNanos(90L));
       assertEquals(Long.valueOf(2L), warningCount.get());
 
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWriterOrderingTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWriterOrderingTest.java
index d943ca41241..972a3a5fdc0 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWriterOrderingTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/consensus/ConsensusPrefetchingQueueWriterOrderingTest.java
@@ -28,6 +28,7 @@ import 
org.apache.iotdb.consensus.iot.SubscriptionWalRetentionPolicy;
 import org.apache.iotdb.consensus.iot.WriterSafeFrontierTracker;
 import org.apache.iotdb.consensus.iot.log.ConsensusReqReader;
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.i18n.DataNodePipeMessages;
 import org.apache.iotdb.db.pipe.resource.memory.PipeMemoryWeightUtil;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowNode;
 import org.apache.iotdb.db.queryengine.plan.statement.StatementTestUtils;
@@ -36,6 +37,7 @@ 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.recover.WALRepairWriter;
 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;
@@ -46,6 +48,10 @@ import 
org.apache.iotdb.rpc.subscription.payload.poll.RegionProgress;
 import 
org.apache.iotdb.rpc.subscription.payload.poll.SubscriptionPollResponseType;
 import org.apache.iotdb.rpc.subscription.payload.poll.TabletsPayload;
 
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
 import org.apache.tsfile.enums.TSDataType;
 import org.apache.tsfile.write.record.Tablet;
 import org.apache.tsfile.write.schema.MeasurementSchema;
@@ -54,16 +60,20 @@ import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
+import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
 import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.junit.Assert.assertEquals;
@@ -116,6 +126,83 @@ public class ConsensusPrefetchingQueueWriterOrderingTest {
     IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
   }
 
+  @Test
+  public void testRecoveredUnknownWriterLogsActualBlockingBarrier() throws 
Exception {
+    final Logger logger = (Logger) 
LoggerFactory.getLogger(ConsensusPrefetchingQueue.class);
+    final Level originalLevel = logger.getLevel();
+    final ListAppender<ILoggingEvent> appender = new ListAppender<>();
+    logger.setLevel(Level.WARN);
+    appender.start();
+    logger.addAppender(appender);
+    try {
+      final File directory = writeWal(8);
+      final File data =
+          new File(
+              directory, WALFileUtils.getLogFileName(0, 0, 
WALFileStatus.CONTAINS_SEARCH_INDEX));
+      try (FileChannel channel = FileChannel.open(data.toPath(), 
StandardOpenOption.WRITE)) {
+        channel.truncate(channel.size() - 1);
+      }
+      final WALMetaData recovered;
+      try (FileChannel channel = FileChannel.open(data.toPath(), 
StandardOpenOption.READ)) {
+        recovered = WALMetaData.readFromWALFile(data, channel);
+      }
+      assertTrue(new WALRepairWriter(data).repair(recovered));
+      newQueue(walNode(directory, 8), 1024 * 1024);
+      queue.setActiveWriterNodeIds(Set.of(7));
+      for (int round = 0; round < 5; round++) {
+        queue.drivePrefetchOnce();
+      }
+      assertNull(queue.poll("consumer"));
+      final int readsBefore = walReads.get();
+      final long bytesBefore = queue.getRetainedTabletBytes();
+      final Field iteratorField =
+          
ConsensusPrefetchingQueue.class.getDeclaredField("subscriptionWALIterator");
+      iteratorField.setAccessible(true);
+      final ProgressWALIterator iterator = (ProgressWALIterator) 
iteratorField.get(queue);
+      assertNotNull(iterator);
+      final String scanStateBefore = iterator.getDiagnosticState();
+      final Method report =
+          ConsensusPrefetchingQueue.class.getDeclaredMethod(
+              "reportReplayStallIfNecessary", long.class);
+      report.setAccessible(true);
+      final long nowNanos = System.nanoTime();
+      report.invoke(queue, nowNanos);
+      report.invoke(queue, nowNanos + TimeUnit.SECONDS.toNanos(60));
+      final ILoggingEvent warning =
+          appender.list.stream()
+              .filter(
+                  event ->
+                      event
+                          .getMessage()
+                          .equals(
+                              DataNodePipeMessages
+                                  
.LOG_SUBSCRIPTION_REPLAY_STALLED_FOR_ARG_MS_ARG_DIAGNOSTICS_ARG_INSPECT_PREFETCH_WORKER_STACKS_AND_WAL_I_O_CB012F98))
+              .findFirst()
+              .get();
+      final String diagnostic = warning.getFormattedMessage();
+      assertTrue(
+          diagnostic, 
diagnostic.contains("lastBlockedFrontier=WriterFrontier{writerNodeId=7"));
+      assertTrue(diagnostic, diagnostic.contains("orderingKey=(" + 
Long.MIN_VALUE + ","));
+      assertTrue(diagnostic, diagnostic.contains("barrier=true"));
+      // The replicated entry also loses its progress and sorts before the 
local defaults.
+      assertTrue(diagnostic, diagnostic.contains("bufferedHead=(0,-1,-1)"));
+      assertTrue(diagnostic, diagnostic.contains("bufferedSearchIndex=-1"));
+      assertTrue(diagnostic, diagnostic.contains("bufferedEntryCount=9"));
+      assertTrue(diagnostic, 
diagnostic.contains("admissionBlockReason=WRITER_BACKLOG"));
+      assertTrue(diagnostic, 
diagnostic.contains("retentionContainsUnsupportedProgress=true"));
+      assertTrue(diagnostic, diagnostic.contains(directory.toString()));
+      // Taking the diagnostic must not consume WAL requests, change ordering, 
or retain Tablets.
+      assertEquals(readsBefore, walReads.get());
+      assertEquals(scanStateBefore, iterator.getDiagnosticState());
+      assertEquals(bytesBefore, queue.getRetainedTabletBytes());
+      assertNull(queue.poll("consumer"));
+    } finally {
+      logger.detachAppender(appender);
+      logger.setLevel(originalLevel);
+      appender.stop();
+    }
+  }
+
   @Test
   public void testBlockedWriterBarrierRetriesLaterAndResumesAfterIdleBarrier() 
throws Exception {
     newQueue(mock(ConsensusReqReader.class), 1024 * 1024);

Reply via email to