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 fefbbbcf0fa [Subscription] Balance consensus ownership and report WAL 
backlog (#18641)
fefbbbcf0fa is described below

commit fefbbbcf0fa680f0beb31f200e29d8db1ca6fd0b
Author: Caideyipi <[email protected]>
AuthorDate: Thu Sep 17 09:39:28 2026 +0800

    [Subscription] Balance consensus ownership and report WAL backlog (#18641)
---
 .../broker/ConsensusSubscriptionBroker.java        | 171 +++++++++++++-----
 .../consensus/ConsensusPrefetchingQueue.java       |  18 +-
 .../ConsensusSubscriptionBrokerOwnershipTest.java  | 192 +++++++++++++++++++++
 .../consensus/ConsensusPrefetchingQueueTest.java   |  54 ++++++
 4 files changed, 380 insertions(+), 55 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
index b911478c2b2..4df735a7b76 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBroker.java
@@ -42,6 +42,8 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -516,39 +518,46 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
       final String topicName,
       final List<ConsensusPrefetchingQueue> queues,
       final String consumerId) {
-    final ConcurrentHashMap<String, Long> consumerTimestamps =
-        topicConsumerLastPollMs.computeIfAbsent(topicName, ignored -> new 
ConcurrentHashMap<>());
-    consumerTimestamps.put(consumerId, System.currentTimeMillis());
-    evictInactiveConsumers(consumerTimestamps);
-    final List<String> sortedConsumers = new 
ArrayList<>(consumerTimestamps.keySet());
-    Collections.sort(sortedConsumers);
-
-    final List<String> activeRegionIds =
-        queues.stream()
-            .filter(q -> !q.isClosed())
-            .map(q -> q.getConsensusGroupId().toString())
-            .sorted()
-            .collect(Collectors.toList());
-
-    final TopicOwnershipSnapshot existingSnapshot = 
topicOwnershipSnapshots.get(topicName);
-    if (Objects.nonNull(existingSnapshot)
-        && existingSnapshot.hasSameConsumers(sortedConsumers)
-        && existingSnapshot.hasSameRegions(activeRegionIds)) {
-      return existingSnapshot;
-    }
-
-    final TopicOwnershipSnapshot refreshedSnapshot =
-        TopicOwnershipSnapshot.create(sortedConsumers, activeRegionIds);
-    topicOwnershipSnapshots.put(topicName, refreshedSnapshot);
-    LOGGER.debug(
-        DataNodePipeMessages
-            
.PIPE_LOG_CONSENSUSSUBSCRIPTIONBROKER_REFRESHED_OWNERSHIP_FOR_TOPIC_EB11CF64,
-        brokerId,
-        topicName,
-        sortedConsumers,
-        activeRegionIds,
-        refreshedSnapshot.getGeneration());
-    return refreshedSnapshot;
+    synchronized (queueLifecycleLock) {
+      // Do not recreate ownership metadata for a topic that was concurrently 
removed.
+      if (topicNameToConsensusPrefetchingQueues.get(topicName) != queues) {
+        return TopicOwnershipSnapshot.empty();
+      }
+
+      final ConcurrentHashMap<String, Long> consumerTimestamps =
+          topicConsumerLastPollMs.computeIfAbsent(topicName, ignored -> new 
ConcurrentHashMap<>());
+      consumerTimestamps.put(consumerId, System.currentTimeMillis());
+      evictInactiveConsumers(consumerTimestamps);
+      final List<String> sortedConsumers = new 
ArrayList<>(consumerTimestamps.keySet());
+      Collections.sort(sortedConsumers);
+
+      final List<String> activeRegionIds =
+          queues.stream()
+              .filter(q -> !q.isClosed())
+              .map(q -> q.getConsensusGroupId().toString())
+              .sorted()
+              .collect(Collectors.toList());
+
+      final TopicOwnershipSnapshot existingSnapshot = 
topicOwnershipSnapshots.get(topicName);
+      if (Objects.nonNull(existingSnapshot)
+          && existingSnapshot.hasSameConsumers(sortedConsumers)
+          && existingSnapshot.hasSameRegions(activeRegionIds)) {
+        return existingSnapshot;
+      }
+
+      final TopicOwnershipSnapshot refreshedSnapshot =
+          TopicOwnershipSnapshot.create(sortedConsumers, activeRegionIds, 
existingSnapshot);
+      topicOwnershipSnapshots.put(topicName, refreshedSnapshot);
+      LOGGER.debug(
+          DataNodePipeMessages
+              
.PIPE_LOG_CONSENSUSSUBSCRIPTIONBROKER_REFRESHED_OWNERSHIP_FOR_TOPIC_EB11CF64,
+          brokerId,
+          topicName,
+          sortedConsumers,
+          activeRegionIds,
+          refreshedSnapshot.getGeneration());
+      return refreshedSnapshot;
+    }
   }
 
   private List<ConsensusPrefetchingQueue> getAssignedQueues(
@@ -727,8 +736,6 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
           topicNameToConsensusPrefetchingQueues.remove(entry.getKey(), queues);
           topicConsumerLastPollMs.remove(entry.getKey());
           topicOwnershipSnapshots.remove(entry.getKey());
-        } else {
-          topicOwnershipSnapshots.remove(entry.getKey());
         }
       }
     }
@@ -844,7 +851,7 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
         brokerId);
   }
 
-  private static final class TopicOwnershipSnapshot {
+  static final class TopicOwnershipSnapshot {
 
     private final List<String> activeConsumers;
     private final List<String> activeRegionIds;
@@ -862,19 +869,70 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
       this.generation = generation;
     }
 
-    private static TopicOwnershipSnapshot create(
-        final List<String> activeConsumers, final List<String> 
activeRegionIds) {
+    static TopicOwnershipSnapshot create(
+        final List<String> activeConsumers,
+        final List<String> activeRegionIds,
+        final TopicOwnershipSnapshot previousSnapshot) {
       if (activeConsumers.isEmpty() || activeRegionIds.isEmpty()) {
         return new TopicOwnershipSnapshot(
-            Collections.emptyList(), Collections.emptyList(), 
Collections.emptyMap(), 0);
+            Collections.unmodifiableList(new ArrayList<>(activeConsumers)),
+            Collections.unmodifiableList(new ArrayList<>(activeRegionIds)),
+            Collections.emptyMap(),
+            0);
       }
 
-      final Map<String, String> ownerByRegionId = new ConcurrentHashMap<>();
-      final int consumerCount = activeConsumers.size();
-      for (final String regionId : activeRegionIds) {
-        final int ownerIdx = Math.floorMod(regionId.hashCode(), consumerCount);
-        ownerByRegionId.put(regionId, activeConsumers.get(ownerIdx));
+      final Map<String, String> ownerByRegionId = new LinkedHashMap<>();
+      final Map<String, Integer> regionCountByConsumer = new HashMap<>();
+      activeConsumers.forEach(consumer -> regionCountByConsumer.put(consumer, 
0));
+
+      // Keep assignments that are still valid. Reassigning every region 
whenever membership
+      // changes causes consumers to repeatedly lose their WAL queues and 
makes empty polls likely.
+      if (Objects.nonNull(previousSnapshot)) {
+        for (final String regionId : activeRegionIds) {
+          final String owner = previousSnapshot.getOwnerConsumerId(regionId);
+          if (Objects.nonNull(owner) && 
regionCountByConsumer.containsKey(owner)) {
+            ownerByRegionId.put(regionId, owner);
+            regionCountByConsumer.computeIfPresent(owner, (ignored, count) -> 
count + 1);
+          }
+        }
+      }
+
+      final List<String> unassignedRegionIds =
+          activeRegionIds.stream()
+              .filter(regionId -> !ownerByRegionId.containsKey(regionId))
+              .collect(Collectors.toCollection(ArrayList::new));
+
+      // Assign newly created regions, or regions whose owner left, before 
moving valid ownership.
+      for (final String regionId : unassignedRegionIds) {
+        final String leastLoadedConsumer =
+            findLeastLoadedConsumer(activeConsumers, regionCountByConsumer);
+        ownerByRegionId.put(regionId, leastLoadedConsumer);
+        regionCountByConsumer.computeIfPresent(leastLoadedConsumer, (ignored, 
count) -> count + 1);
+      }
+
+      // Move only enough valid ownerships to make the distribution balanced. 
Choosing consumers
+      // and regions deterministically keeps ownership stable across JVMs.
+      while (true) {
+        final String leastLoadedConsumer =
+            findLeastLoadedConsumer(activeConsumers, regionCountByConsumer);
+        final String mostLoadedConsumer =
+            findMostLoadedConsumer(activeConsumers, regionCountByConsumer);
+        if (regionCountByConsumer.get(mostLoadedConsumer)
+                - regionCountByConsumer.get(leastLoadedConsumer)
+            <= 1) {
+          break;
+        }
+
+        final String regionToMove =
+            activeRegionIds.stream()
+                .filter(regionId -> 
mostLoadedConsumer.equals(ownerByRegionId.get(regionId)))
+                .max(Comparator.naturalOrder())
+                .orElseThrow(IllegalStateException::new);
+        ownerByRegionId.put(regionToMove, leastLoadedConsumer);
+        regionCountByConsumer.computeIfPresent(mostLoadedConsumer, (ignored, 
count) -> count - 1);
+        regionCountByConsumer.computeIfPresent(leastLoadedConsumer, (ignored, 
count) -> count + 1);
       }
+
       return new TopicOwnershipSnapshot(
           Collections.unmodifiableList(new ArrayList<>(activeConsumers)),
           Collections.unmodifiableList(new ArrayList<>(activeRegionIds)),
@@ -882,6 +940,29 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
           ownerByRegionId.hashCode());
     }
 
+    private static TopicOwnershipSnapshot empty() {
+      return new TopicOwnershipSnapshot(
+          Collections.emptyList(), Collections.emptyList(), 
Collections.emptyMap(), 0);
+    }
+
+    private static String findLeastLoadedConsumer(
+        final List<String> activeConsumers, final Map<String, Integer> 
regionCountByConsumer) {
+      return activeConsumers.stream()
+          .min(
+              Comparator.comparingInt((String consumer) -> 
regionCountByConsumer.get(consumer))
+                  .thenComparing(Comparator.naturalOrder()))
+          .orElseThrow(IllegalStateException::new);
+    }
+
+    private static String findMostLoadedConsumer(
+        final List<String> activeConsumers, final Map<String, Integer> 
regionCountByConsumer) {
+      return activeConsumers.stream()
+          .min(
+              Comparator.comparingInt((String consumer) -> 
-regionCountByConsumer.get(consumer))
+                  .thenComparing(Comparator.naturalOrder()))
+          .orElseThrow(IllegalStateException::new);
+    }
+
     private boolean isEmpty() {
       return activeConsumers.isEmpty() || activeRegionIds.isEmpty();
     }
@@ -894,7 +975,7 @@ public class ConsensusSubscriptionBroker implements 
ISubscriptionBroker {
       return activeRegionIds.equals(regionIds);
     }
 
-    private String getOwnerConsumerId(final String regionId) {
+    String getOwnerConsumerId(final String regionId) {
       return ownerByRegionId.get(regionId);
     }
 
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 c6fd52301a4..ecb01d2868e 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
@@ -4258,21 +4258,19 @@ public class ConsensusPrefetchingQueue {
   /**
    * Returns the queue-local lag used by metrics.
    *
-   * <p>Events that have already been materialized in memory are counted 
exactly. For data that is
-   * still only in WAL, the exact number is not tracked by this queue and 
computing it would require
-   * scanning WAL only for reporting. Therefore, unread WAL data is 
represented as one extra unit,
-   * so the metric shows that this queue is not caught up without turning lag 
reporting into another
-   * WAL reader.
+   * <p>Entries in the materialized lifecycle stages have already advanced the 
WAL cursor. Pending
+   * entries have not, so they overlap with the raw WAL search-index gap. 
Taking the maximum for the
+   * unmaterialized part avoids double-counting that overlap while still 
exposing a large unread WAL
+   * backlog instead of collapsing it to one unit.
    */
   public long getLag() {
-    final long queuedLag =
-        prefetchingQueue.size()
+    final long materializedLag =
+        (long) prefetchingQueue.size()
             + inFlightEvents.size()
-            + pendingEntries.size()
             + getRealtimeBufferedEntryCount()
             + lingerBatch.getEntryCount();
-    final boolean hasUnreadWalEntries = hasUnreadWalEntriesBehindCursor();
-    return queuedLag + (hasUnreadWalEntries ? 1 : 0);
+    final long unmaterializedLag = Math.max((long) pendingEntries.size(), 
getRawWalGap());
+    return materializedLag + unmaterializedLag;
   }
 
   // ======================== Stringify ========================
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBrokerOwnershipTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBrokerOwnershipTest.java
new file mode 100644
index 00000000000..99bdb83fdd9
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/broker/ConsensusSubscriptionBrokerOwnershipTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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;
+
+import 
org.apache.iotdb.db.subscription.broker.ConsensusSubscriptionBroker.TopicOwnershipSnapshot;
+
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class ConsensusSubscriptionBrokerOwnershipTest {
+
+  @Test
+  public void testEqualNumbersOfConsumersAndRegionsAssignEveryConsumer() {
+    final List<String> consumers = consumerIds(40);
+    final List<String> regions = regionIds(3, 42);
+
+    final TopicOwnershipSnapshot snapshot = 
TopicOwnershipSnapshot.create(consumers, regions, null);
+    final Map<String, Integer> loads = loads(snapshot, consumers, regions);
+
+    assertEquals(40, loads.size());
+    assertTrue(loads.values().stream().allMatch(load -> load == 1));
+  }
+
+  @Test
+  public void testJoiningConsumerTriggersOnlyRequiredMoves() {
+    final List<String> regions = regionIds(3, 42);
+    final TopicOwnershipSnapshot oneConsumer =
+        TopicOwnershipSnapshot.create(Collections.singletonList("consumer_1"), 
regions, null);
+    final TopicOwnershipSnapshot twoConsumers =
+        TopicOwnershipSnapshot.create(
+            Arrays.asList("consumer_1", "consumer_2"), regions, oneConsumer);
+
+    assertEquals(20, movedRegionCount(oneConsumer, twoConsumers, regions));
+    assertBalanced(twoConsumers, Arrays.asList("consumer_1", "consumer_2"), 
regions);
+
+    final TopicOwnershipSnapshot threeConsumers =
+        TopicOwnershipSnapshot.create(
+            Arrays.asList("consumer_1", "consumer_2", "consumer_3"), regions, 
twoConsumers);
+    assertEquals(13, movedRegionCount(twoConsumers, threeConsumers, regions));
+    assertBalanced(
+        threeConsumers, Arrays.asList("consumer_1", "consumer_2", 
"consumer_3"), regions);
+  }
+
+  @Test
+  public void testLeavingConsumerOnlyReassignsItsRegions() {
+    final List<String> consumers =
+        Arrays.asList("consumer_1", "consumer_2", "consumer_3", "consumer_4");
+    final List<String> regions = regionIds(3, 42);
+    final TopicOwnershipSnapshot before = 
TopicOwnershipSnapshot.create(consumers, regions, null);
+    final List<String> remainingConsumers = Arrays.asList("consumer_1", 
"consumer_3", "consumer_4");
+    final TopicOwnershipSnapshot after =
+        TopicOwnershipSnapshot.create(remainingConsumers, regions, before);
+
+    for (final String region : regions) {
+      if (!"consumer_2".equals(before.getOwnerConsumerId(region))) {
+        assertEquals(before.getOwnerConsumerId(region), 
after.getOwnerConsumerId(region));
+      }
+    }
+    assertEquals(10, movedRegionCount(before, after, regions));
+    assertBalanced(after, remainingConsumers, regions);
+  }
+
+  @Test
+  public void testRegionChangesPreserveValidOwnership() {
+    final List<String> consumers = Arrays.asList("consumer_1", "consumer_2", 
"consumer_3");
+    final List<String> initialRegions = regionIds(3, 11);
+    final TopicOwnershipSnapshot initial =
+        TopicOwnershipSnapshot.create(consumers, initialRegions, null);
+
+    final List<String> expandedRegions = new ArrayList<>(initialRegions);
+    expandedRegions.addAll(regionIds(12, 14));
+    final TopicOwnershipSnapshot expanded =
+        TopicOwnershipSnapshot.create(consumers, expandedRegions, initial);
+    assertEquals(0, movedRegionCount(initial, expanded, initialRegions));
+    assertBalanced(expanded, consumers, expandedRegions);
+
+    final List<String> reducedRegions = new ArrayList<>(expandedRegions);
+    reducedRegions.remove("DataRegion[12]");
+    reducedRegions.remove("DataRegion[13]");
+    reducedRegions.remove("DataRegion[14]");
+    final TopicOwnershipSnapshot reduced =
+        TopicOwnershipSnapshot.create(consumers, reducedRegions, expanded);
+    assertEquals(0, movedRegionCount(expanded, reduced, reducedRegions));
+    assertBalanced(reduced, consumers, reducedRegions);
+  }
+
+  @Test
+  public void 
testMoreConsumersThanRegionsLeavesOnlyUnavoidableConsumersEmpty() {
+    final List<String> consumers = consumerIds(5);
+    final List<String> regions = regionIds(3, 5);
+
+    final TopicOwnershipSnapshot snapshot = 
TopicOwnershipSnapshot.create(consumers, regions, null);
+    final Map<String, Integer> loads = loads(snapshot, consumers, regions);
+
+    assertEquals(3, loads.values().stream().filter(load -> load == 1).count());
+    assertEquals(2, loads.values().stream().filter(load -> load == 0).count());
+    assertBalanced(snapshot, consumers, regions);
+  }
+
+  @Test
+  public void testEmptyInputsProduceEmptyOwnership() {
+    final TopicOwnershipSnapshot noConsumers =
+        TopicOwnershipSnapshot.create(
+            Collections.emptyList(), 
Collections.singletonList("DataRegion[3]"), null);
+    assertNull(noConsumers.getOwnerConsumerId("DataRegion[3]"));
+
+    final TopicOwnershipSnapshot noRegions =
+        TopicOwnershipSnapshot.create(
+            Collections.singletonList("consumer_1"), Collections.emptyList(), 
null);
+    assertNull(noRegions.getOwnerConsumerId("DataRegion[3]"));
+  }
+
+  private static List<String> consumerIds(final int count) {
+    return IntStream.rangeClosed(1, count)
+        .mapToObj(index -> "consumer_" + index)
+        .sorted()
+        .collect(Collectors.toList());
+  }
+
+  private static List<String> regionIds(final int startInclusive, final int 
endInclusive) {
+    return IntStream.rangeClosed(startInclusive, endInclusive)
+        .mapToObj(index -> "DataRegion[" + index + "]")
+        .sorted()
+        .collect(Collectors.toList());
+  }
+
+  private static Map<String, Integer> loads(
+      final TopicOwnershipSnapshot snapshot,
+      final List<String> consumers,
+      final List<String> regions) {
+    final Map<String, Integer> result = new HashMap<>();
+    consumers.forEach(consumer -> result.put(consumer, 0));
+    for (final String region : regions) {
+      result.computeIfPresent(snapshot.getOwnerConsumerId(region), (ignored, 
count) -> count + 1);
+    }
+    return result;
+  }
+
+  private static void assertBalanced(
+      final TopicOwnershipSnapshot snapshot,
+      final List<String> consumers,
+      final List<String> regions) {
+    final Map<String, Integer> loads = loads(snapshot, consumers, regions);
+    assertFalse(loads.isEmpty());
+    final int minimumLoad = Collections.min(loads.values());
+    final int maximumLoad = Collections.max(loads.values());
+    assertTrue(maximumLoad - minimumLoad <= 1);
+    assertEquals(regions.size(), 
loads.values().stream().mapToInt(Integer::intValue).sum());
+  }
+
+  private static int movedRegionCount(
+      final TopicOwnershipSnapshot before,
+      final TopicOwnershipSnapshot after,
+      final List<String> regions) {
+    return (int)
+        regions.stream()
+            .filter(
+                region ->
+                    
!before.getOwnerConsumerId(region).equals(after.getOwnerConsumerId(region)))
+            .count();
+  }
+}
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 a6d76a8d98a..16182cae924 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
@@ -395,6 +395,7 @@ public class ConsensusPrefetchingQueueTest {
               1L,
               1L,
               true);
+      queue.setSubscriptionMemoryManager(new SubscriptionMemoryManager(16L * 
1024 * 1024));
       final IndexedConsensusRequest request =
           new IndexedConsensusRequest(
                   1L, 
Collections.singletonList(StatementTestUtils.genInsertRowNode(1)))
@@ -434,6 +435,59 @@ public class ConsensusPrefetchingQueueTest {
     }
   }
 
+  @Test
+  public void testLagIncludesUnreadWalSearchIndexDistance() throws Exception {
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final File systemDir = temporaryFolder.newFolder("lagWithUnreadWal");
+    ConsensusPrefetchingQueue queue = null;
+    try {
+      final DataRegionId regionId = new DataRegionId(9);
+      final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+      reader.currentSearchIndex = 300_000L;
+      final IoTConsensusServerImpl serverImpl = 
mock(IoTConsensusServerImpl.class);
+      when(serverImpl.getConsensusReqReader()).thenReturn(reader);
+      when(serverImpl.getWriterSafeFrontierTracker()).thenReturn(new 
WriterSafeFrontierTracker());
+      final ConsensusLogToTabletConverter converter = 
mock(ConsensusLogToTabletConverter.class);
+      when(converter.getDatabaseName()).thenReturn("db");
+      
when(converter.convert(any())).thenReturn(Collections.singletonList(createTablet()));
+      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);
+      queue.setSubscriptionMemoryManager(new SubscriptionMemoryManager(16L * 
1024 * 1024));
+
+      assertEquals(300_000L, queue.getRawWalGap());
+      assertEquals(300_000L, queue.getLag());
+
+      assertNull(queue.poll("consumer"));
+      assertTrue(pendingEntries(queue).offer(createRequest(1L)));
+      queue.drivePrefetchOnce();
+
+      assertEquals(2L, queue.getCurrentReadSearchIndex());
+      assertEquals(299_999L, queue.getRawWalGap());
+      assertEquals(1L, queue.getRemainingEventCount());
+      assertEquals(300_000L, queue.getLag());
+    } finally {
+      if (queue != null) {
+        queue.close();
+      }
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
   @Test
   public void testFilteredEmptyEntryAdvancesProgressWithoutEvent() throws 
Exception {
     final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();

Reply via email to