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 4e05c9f8709 Avoid subscription WAL segment reloads across prefetch 
slices (#18960)
4e05c9f8709 is described below

commit 4e05c9f8709db9f1e3ec8d4b044606412c7f71e1
Author: Caideyipi <[email protected]>
AuthorDate: Wed Oct 21 18:20:42 2026 +0800

    Avoid subscription WAL segment reloads across prefetch slices (#18960)
    
    * Retain WAL segment buffers across immediate prefetch rounds
    
    * Fix SystemInfoTest for write memory manager API
---
 .../consensus/ConsensusPrefetchingQueue.java       |  42 +++--
 .../rescon/memory/SystemInfoTest.java              |  24 +--
 .../consensus/ConsensusPrefetchingQueueTest.java   | 182 +++++++++++++++++++++
 3 files changed, 222 insertions(+), 26 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 3f0561d37f6..1268689749f 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
@@ -1758,6 +1758,34 @@ public class ConsensusPrefetchingQueue {
     }
 
     acquireReadLock();
+    PrefetchRoundResult result = null;
+    try {
+      result = drivePrefetchOnceUnderReadLock();
+      return result;
+    } finally {
+      try {
+        final ProgressWALIterator iterator = subscriptionWALIterator;
+        walReplayState = Objects.nonNull(iterator) ? iterator.toString() : 
"UNAVAILABLE";
+        // Keep unread segments across immediately rescheduled scan slices. 
Releasing them here
+        // would reload and decompress the same segment after every entry/time 
budget yield.
+        // Paused queues still return their direct memory while retaining 
cursors and fragments.
+        if (Objects.isNull(result) || result.getType() != 
PrefetchRoundResult.Type.RESCHEDULE_NOW) {
+          if (Objects.nonNull(iterator)) {
+            iterator.releaseReadBuffers();
+          }
+          for (final WriterReplayState state : orderedWriterReplay.values()) {
+            if (Objects.nonNull(state.iterator)) {
+              state.iterator.releaseReadBuffers();
+            }
+          }
+        }
+      } finally {
+        releaseReadLock();
+      }
+    }
+  }
+
+  private PrefetchRoundResult drivePrefetchOnceUnderReadLock() {
     try {
       final ProgressWALIterator.ScanBudget walScanBudget = newWalScanBudget();
       if (isClosed || closeRequested || !prefetchInitialized) {
@@ -1945,20 +1973,6 @@ public class ConsensusPrefetchingQueue {
         return PrefetchRoundResult.dormant();
       }
       return PrefetchRoundResult.rescheduleAfter(100L);
-    } finally {
-      final ProgressWALIterator iterator = subscriptionWALIterator;
-      walReplayState = Objects.nonNull(iterator) ? iterator.toString() : 
"UNAVAILABLE";
-      // Readers outlive a round, but their direct segment buffers must not be 
retained by every
-      // inactive/backpressured queue. Keep their cursors and request 
fragments for the next round.
-      if (Objects.nonNull(iterator)) {
-        iterator.releaseReadBuffers();
-      }
-      for (final WriterReplayState state : orderedWriterReplay.values()) {
-        if (Objects.nonNull(state.iterator)) {
-          state.iterator.releaseReadBuffers();
-        }
-      }
-      releaseReadLock();
     }
   }
 
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/rescon/memory/SystemInfoTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/rescon/memory/SystemInfoTest.java
index 8c5e3e362d5..d330af1777e 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/rescon/memory/SystemInfoTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/rescon/memory/SystemInfoTest.java
@@ -86,7 +86,7 @@ public class SystemInfoTest {
 
   @Test
   public void testRejectedWriterCannotFlushSmallerCurrentProcessor() throws 
Exception {
-    dataRegionInfo.addStorageGroupMemCost(900);
+    dataRegionInfo.getWriteMemoryManager().addMemory(900);
 
     Assert.assertFalse(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
     Assert.assertTrue(systemInfo.isRejected());
@@ -97,7 +97,7 @@ public class SystemInfoTest {
   @Test
   public void testRejectedWriterFirstTabletCannotBypassSelection() throws 
Exception {
     when(currentProcessor.getWorkMemTableRamCost()).thenReturn(0L);
-    dataRegionInfo.addStorageGroupMemCost(900);
+    dataRegionInfo.getWriteMemoryManager().addMemory(900);
 
     Assert.assertFalse(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
     verify(currentProcessor, never()).setWorkMemTableShouldFlush();
@@ -106,7 +106,7 @@ public class SystemInfoTest {
 
   @Test
   public void testRepeatedReportsSubmitEachMemtableOnce() throws Exception {
-    dataRegionInfo.addStorageGroupMemCost(900);
+    dataRegionInfo.getWriteMemoryManager().addMemory(900);
 
     for (int i = 0; i < 10; i++) {
       Assert.assertFalse(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
@@ -120,7 +120,7 @@ public class SystemInfoTest {
   @Test
   public void testRepeatedReportsForFirstTabletSubmitOnce() throws Exception {
     when(currentProcessor.getWorkMemTableRamCost()).thenReturn(0L);
-    dataRegionInfo.addStorageGroupMemCost(900);
+    dataRegionInfo.getWriteMemoryManager().addMemory(900);
 
     for (int i = 0; i < 10; i++) {
       Assert.assertFalse(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
@@ -134,7 +134,7 @@ public class SystemInfoTest {
   public void testNewMemtableCanBeScheduledAfterPreviousFlush() throws 
Exception {
     when(currentProcessor.getWorkMemTableRamCost()).thenReturn(700L);
     when(largerProcessor.getWorkMemTableRamCost()).thenReturn(100L);
-    dataRegionInfo.addStorageGroupMemCost(900);
+    dataRegionInfo.getWriteMemoryManager().addMemory(900);
     Assert.assertTrue(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
 
     mockWorkMemTable(currentProcessor);
@@ -148,9 +148,9 @@ public class SystemInfoTest {
   public void testMarkedCurrentProcessorCannotReachHardMemoryLimit() throws 
Exception {
     when(currentProcessor.getWorkMemTableRamCost()).thenReturn(700L);
     when(largerProcessor.getWorkMemTableRamCost()).thenReturn(100L);
-    dataRegionInfo.addStorageGroupMemCost(900);
+    dataRegionInfo.getWriteMemoryManager().addMemory(900);
     Assert.assertTrue(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
-    dataRegionInfo.addStorageGroupMemCost(100);
+    dataRegionInfo.getWriteMemoryManager().addMemory(100);
 
     try {
       systemInfo.reportStorageGroupStatus(dataRegionInfo, currentProcessor);
@@ -166,7 +166,7 @@ public class SystemInfoTest {
   public void testNoCurrentMemtableCannotBypassRejection() throws Exception {
     when(currentProcessor.getWorkMemTable()).thenReturn(null);
     when(currentProcessor.getWorkMemTableRamCost()).thenReturn(0L);
-    dataRegionInfo.addStorageGroupMemCost(900);
+    dataRegionInfo.getWriteMemoryManager().addMemory(900);
 
     Assert.assertFalse(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
     Assert.assertTrue(systemInfo.isRejected());
@@ -176,10 +176,10 @@ public class SystemInfoTest {
 
   @Test
   public void testRejectionClearsAfterMemoryIsReleased() throws Exception {
-    dataRegionInfo.addStorageGroupMemCost(900);
+    dataRegionInfo.getWriteMemoryManager().addMemory(900);
     Assert.assertFalse(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
 
-    dataRegionInfo.releaseStorageGroupMemCost(500);
+    dataRegionInfo.getWriteMemoryManager().releaseMemory(500);
     systemInfo.resetStorageGroupStatus(dataRegionInfo);
     Assert.assertFalse(systemInfo.isRejected());
     Assert.assertTrue(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
@@ -190,7 +190,7 @@ public class SystemInfoTest {
   public void testCurrentProcessorCannotExceedHardMemoryLimit() throws 
Exception {
     when(currentProcessor.getWorkMemTableRamCost()).thenReturn(700L);
     when(largerProcessor.getWorkMemTableRamCost()).thenReturn(100L);
-    dataRegionInfo.addStorageGroupMemCost(1100);
+    dataRegionInfo.getWriteMemoryManager().addMemory(1100);
 
     try {
       systemInfo.reportStorageGroupStatus(dataRegionInfo, currentProcessor);
@@ -202,7 +202,7 @@ public class SystemInfoTest {
 
   @Test
   public void testNormalFlushStillSelectsLargestMemtable() throws Exception {
-    dataRegionInfo.addStorageGroupMemCost(700);
+    dataRegionInfo.getWriteMemoryManager().addMemory(700);
 
     Assert.assertTrue(systemInfo.reportStorageGroupStatus(dataRegionInfo, 
currentProcessor));
     Assert.assertFalse(systemInfo.isRejected());
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 7ee54b5f830..a985a0f617d 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
@@ -24,6 +24,7 @@ import org.apache.iotdb.commons.consensus.DataRegionId;
 import org.apache.iotdb.commons.memory.AtomicLongMemoryBlock;
 import org.apache.iotdb.commons.memory.IMemoryBlock;
 import org.apache.iotdb.commons.pipe.config.constant.SystemConstant;
+import 
org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeType;
 import org.apache.iotdb.commons.request.IConsensusRequest;
 import org.apache.iotdb.commons.subscription.meta.topic.TopicMeta;
 import org.apache.iotdb.consensus.common.request.IndexedConsensusRequest;
@@ -38,6 +39,9 @@ import 
org.apache.iotdb.consensus.iot.subscription.SubscriptionQueueRejectionRea
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
 import org.apache.iotdb.db.i18n.DataNodePipeMessages;
 import org.apache.iotdb.db.queryengine.plan.statement.StatementTestUtils;
+import org.apache.iotdb.db.service.metrics.WritingMetrics;
+import org.apache.iotdb.db.storageengine.dataregion.wal.WALTestUtils;
+import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntryType;
 import org.apache.iotdb.db.storageengine.dataregion.wal.io.ProgressWALReader;
 import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALFileVersion;
 import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALMetaData;
@@ -51,6 +55,7 @@ import 
org.apache.iotdb.db.subscription.event.SubscriptionEvent;
 import org.apache.iotdb.db.subscription.resource.SubscriptionMemoryManager;
 import 
org.apache.iotdb.db.subscription.task.execution.ConsensusSubscriptionPrefetchExecutor;
 import org.apache.iotdb.db.subscription.task.subtask.ConsensusPrefetchSubtask;
+import org.apache.iotdb.metrics.type.Histogram;
 import org.apache.iotdb.rpc.subscription.config.TopicConstant;
 import org.apache.iotdb.rpc.subscription.payload.poll.ErrorPayload;
 import org.apache.iotdb.rpc.subscription.payload.poll.RegionProgress;
@@ -64,6 +69,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent;
 import ch.qos.logback.core.read.ListAppender;
 import org.apache.tsfile.enums.ColumnCategory;
 import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.metadata.enums.CompressionType;
 import org.apache.tsfile.utils.Pair;
 import org.apache.tsfile.write.record.Tablet;
 import org.junit.Rule;
@@ -1913,6 +1919,182 @@ public class ConsensusPrefetchingQueueTest {
     }
   }
 
+  @Test
+  public void testPausedRoundsReleaseWalReadBuffers() throws Exception {
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final File dir = temporaryFolder.newFolder("paused-wal-buffers");
+    final IoTConsensusServerImpl server = mock(IoTConsensusServerImpl.class);
+    when(server.getConsensusReqReader()).thenReturn(new 
FakeConsensusReqReader());
+    when(server.getWriterSafeFrontierTracker()).thenReturn(new 
WriterSafeFrontierTracker());
+    final ConsensusPrefetchingQueue queue =
+        new ConsensusPrefetchingQueue(
+            "consumerGroup",
+            "topic",
+            TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+            new DataRegionId(12),
+            server,
+            new SubscriptionWalRetentionPolicy(
+                "topic",
+                SubscriptionWalRetentionPolicy.UNBOUNDED,
+                SubscriptionWalRetentionPolicy.UNBOUNDED),
+            mock(ConsensusLogToTabletConverter.class),
+            newCommitManager(dir),
+            new RegionProgress(Collections.emptyMap()),
+            1L,
+            1L,
+            true);
+    try {
+      queue.setSubscriptionMemoryManager(new SubscriptionMemoryManager(0));
+      final Field initialized =
+          
ConsensusPrefetchingQueue.class.getDeclaredField("prefetchInitialized");
+      initialized.setAccessible(true);
+      initialized.setBoolean(queue, true);
+      final Field blockedEntryBytes =
+          
ConsensusPrefetchingQueue.class.getDeclaredField("memoryBlockedEntryBytes");
+      blockedEntryBytes.setAccessible(true);
+      blockedEntryBytes.setLong(queue, 1L);
+      final ProgressWALIterator iterator = mock(ProgressWALIterator.class);
+      setSubscriptionWalIterator(queue, iterator);
+
+      assertEquals(PrefetchRoundResult.Type.RESCHEDULE_LATER, 
queue.drivePrefetchOnce().getType());
+      verify(iterator).releaseReadBuffers();
+      verify(iterator, 
never()).hasNext(any(ProgressWALIterator.ScanBudget.class));
+
+      initialized.setBoolean(queue, false);
+      assertEquals(PrefetchRoundResult.Type.DORMANT, 
queue.drivePrefetchOnce().getType());
+      verify(iterator, times(2)).releaseReadBuffers();
+    } finally {
+      queue.close();
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+    }
+  }
+
+  @Test
+  public void testWalScanSlicesDoNotReloadSegment() throws Exception {
+    final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();
+    final int originalBatchMaxWalEntries =
+        
CommonDescriptor.getInstance().getConfig().getSubscriptionConsensusBatchMaxWalEntries();
+    final CompressionType originalCompression =
+        IoTDBDescriptor.getInstance().getConfig().getWALCompressionAlgorithm();
+    final long originalMinCompressionSize = 
WALTestUtils.getMinCompressionSize();
+    final WritingMetrics metrics = WritingMetrics.getInstance();
+    final Field readSizeField = 
WritingMetrics.class.getDeclaredField("readWALBufferSizeHistogram");
+    final Field uncompressField =
+        WritingMetrics.class.getDeclaredField("walUncompressCostHistogram");
+    readSizeField.setAccessible(true);
+    uncompressField.setAccessible(true);
+    final Object originalReadSize = readSizeField.get(metrics);
+    final Object originalUncompress = uncompressField.get(metrics);
+    try {
+      
CommonDescriptor.getInstance().getConfig().setSubscriptionConsensusBatchMaxWalEntries(1);
+      WALTestUtils.setMinCompressionSize(0);
+      for (final CompressionType compression :
+          new CompressionType[] {CompressionType.UNCOMPRESSED, 
CompressionType.LZ4}) {
+        
IoTDBDescriptor.getInstance().getConfig().setWALCompressionAlgorithm(compression);
+        final File dir = temporaryFolder.newFolder("scan-slices-" + 
compression);
+        final File walFile =
+            new File(dir, WALFileUtils.getLogFileName(0, 1, 
WALFileStatus.CONTAINS_SEARCH_INDEX));
+        final int entryCount = 128;
+        final int entrySize = 1024;
+        final ByteBuffer segment = ByteBuffer.allocate(entryCount * entrySize);
+        final WALMetaData metadata = new WALMetaData();
+        for (int index = 1; index <= entryCount; index++) {
+          segment.put(WALEntryType.INSERT_ROW_NODE.getCode());
+          segment.putLong(1L);
+          segment.putShort(PlanNodeType.INSERT_ROW.getNodeType());
+          segment.putLong(index);
+          segment.position(index * entrySize);
+          metadata.add(entrySize, index, 1L, 1000L + index, 7, index);
+        }
+        try (WALWriter writer = new WALWriter(walFile, WALFileVersion.V3)) {
+          
writer.setCompressedByteBuffer(ByteBuffer.allocate(segment.capacity() * 2));
+          writer.write(segment, metadata);
+        }
+
+        final FakeConsensusReqReader reader = new FakeConsensusReqReader();
+        reader.currentSearchIndex = entryCount;
+        final IoTConsensusServerImpl server = 
mock(IoTConsensusServerImpl.class);
+        when(server.getConsensusReqReader()).thenReturn(reader);
+        when(server.getWriterSafeFrontierTracker()).thenReturn(new 
WriterSafeFrontierTracker());
+        final ConsensusLogToTabletConverter converter = 
mock(ConsensusLogToTabletConverter.class);
+        when(converter.convert(any())).thenReturn(Collections.emptyList());
+        when(converter.getDatabaseName()).thenReturn("db");
+        final AtomicLong bytesRead = new AtomicLong();
+        final Histogram readSizes = mock(Histogram.class);
+        doAnswer(
+                invocation -> {
+                  bytesRead.addAndGet(invocation.getArgument(0));
+                  return null;
+                })
+            .when(readSizes)
+            .update(anyLong());
+        final Histogram uncompressCosts = mock(Histogram.class);
+        readSizeField.set(metrics, readSizes);
+        uncompressField.set(metrics, uncompressCosts);
+        final ConsensusPrefetchingQueue queue =
+            new ConsensusPrefetchingQueue(
+                "consumerGroup",
+                "topic",
+                TopicConstant.ORDER_MODE_LEADER_ONLY_VALUE,
+                new DataRegionId(12),
+                server,
+                new SubscriptionWalRetentionPolicy(
+                    "topic",
+                    SubscriptionWalRetentionPolicy.UNBOUNDED,
+                    SubscriptionWalRetentionPolicy.UNBOUNDED),
+                converter,
+                newCommitManager(
+                    dir,
+                    (group, topic, region) ->
+                        
ConsensusSubscriptionCommitManager.ConfigNodeProgressQueryResult.available(
+                            new RegionProgress(Collections.emptyMap()))),
+                new RegionProgress(Collections.emptyMap()),
+                1L,
+                1L,
+                true) {
+              @Override
+              protected ProgressWALIterator createSubscriptionWALIterator(
+                  final long startSearchIndex) {
+                return new ProgressWALIterator(dir, startSearchIndex);
+              }
+            };
+        queue.setSubscriptionMemoryManager(new SubscriptionMemoryManager(16L * 
1024 * 1024));
+        try {
+          assertNull(queue.poll("consumer"));
+          long firstReadSize = 0;
+          int rounds = 0;
+          while (queue.getCurrentReadSearchIndex() <= entryCount && rounds++ < 
entryCount + 5) {
+            queue.drivePrefetchOnce();
+            if (firstReadSize == 0) {
+              firstReadSize = bytesRead.get();
+            } else {
+              // The segment contains 128 entries, but a round can read only 
one physical entry.
+              // The body must be read/decompressed once, rather than once per 
prefetch round.
+              assertEquals(firstReadSize, bytesRead.get());
+            }
+          }
+          assertTrue(firstReadSize > 0);
+          assertTrue(rounds >= entryCount);
+          assertEquals(entryCount + 1L, queue.getCurrentReadSearchIndex());
+          verify(uncompressCosts, times(compression == CompressionType.LZ4 ? 1 
: 0))
+              .update(anyLong());
+          verify(converter, times(entryCount)).convert(any());
+        } finally {
+          queue.close();
+        }
+      }
+    } finally {
+      readSizeField.set(metrics, originalReadSize);
+      uncompressField.set(metrics, originalUncompress);
+      CommonDescriptor.getInstance()
+          .getConfig()
+          
.setSubscriptionConsensusBatchMaxWalEntries(originalBatchMaxWalEntries);
+      
IoTDBDescriptor.getInstance().getConfig().setWALCompressionAlgorithm(originalCompression);
+      
IoTDBDescriptor.getInstance().getConfig().setSystemDir(originalSystemDir);
+      WALTestUtils.setMinCompressionSize(originalMinCompressionSize);
+    }
+  }
+
   @Test
   public void testPendingGapReplayHonorsPerRoundWalEntryLimit() throws 
Exception {
     final String originalSystemDir = 
IoTDBDescriptor.getInstance().getConfig().getSystemDir();

Reply via email to