This is an automated email from the ASF dual-hosted git repository.

JackieTien97 pushed a commit to branch dev/1.3
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/dev/1.3 by this push:
     new 6e8a31ad2d6 [to dev/1.3] Improve query modification loading memory 
control (#17788) (#18466)
6e8a31ad2d6 is described below

commit 6e8a31ad2d6f074b0ecd298e59b84c8d45404107
Author: shuwenwei <[email protected]>
AuthorDate: Fri Aug 14 10:17:11 2026 +0800

    [to dev/1.3] Improve query modification loading memory control (#17788) 
(#18466)
---
 .../fragment/FragmentInstanceContext.java          |  57 ++++
 .../fragment/QueryModificationLoader.java          | 306 +++++++++++++++++
 .../memory/FakedMemoryReservationManager.java      |   3 +
 .../planner/memory/MemoryReservationManager.java   |   8 +
 .../dataregion/modification/ModificationFile.java  |   5 +-
 .../io/LocalTextModificationAccessor.java          | 124 ++++---
 ...cationReader.java => ModificationIterator.java} |  30 +-
 .../modification/io/ModificationReader.java        |   3 +-
 .../fragment/QueryModificationLoaderTest.java      | 372 +++++++++++++++++++++
 9 files changed, 839 insertions(+), 69 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceContext.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceContext.java
index 14a90159dac..102df82d384 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceContext.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceContext.java
@@ -20,6 +20,7 @@
 package org.apache.iotdb.db.queryengine.execution.fragment;
 
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.exception.IllegalPathException;
 import org.apache.iotdb.commons.exception.IoTDBException;
 import org.apache.iotdb.commons.path.AlignedPath;
 import org.apache.iotdb.commons.path.PartialPath;
@@ -76,6 +77,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 import static 
org.apache.iotdb.db.queryengine.metric.DriverSchedulerMetricSet.BLOCK_QUEUED_TIME;
@@ -91,6 +93,7 @@ public class FragmentInstanceContext extends QueryContext {
   private static final Logger LOGGER = 
LoggerFactory.getLogger(FragmentInstanceContext.class);
   private static final IoTDBConfig CONFIG = 
IoTDBDescriptor.getInstance().getConfig();
   private static final long END_TIME_INITIAL_VALUE = -1L;
+  private static final int MODS_MEMORY_ESTIMATE_READ_INTERVAL = 10_000;
   // wait over 5s for driver to close is abnormal
   private static final long LONG_WAIT_DURATION = 5_000_000_000L;
   private final FragmentInstanceId id;
@@ -399,6 +402,60 @@ public class FragmentInstanceContext extends QueryContext {
     lastExecutionStartTime.set(now);
   }
 
+  @Override
+  public List<Modification> getPathModifications(
+      TsFileResource tsFileResource, IDeviceID deviceID, String measurement)
+      throws IllegalPathException {
+    if (!checkIfModificationExists(tsFileResource)) {
+      return Collections.emptyList();
+    }
+    if (memoryReservationManager == null) {
+      return super.getPathModifications(tsFileResource, deviceID, measurement);
+    }
+    PartialPath path = new PartialPath(deviceID, measurement);
+    try (QueryModificationLoader modificationLoader =
+        getQueryModificationLoader(
+            tsFileResource,
+            modification -> modification.getPath().overlapWith(path),
+            mods -> getPathModifications(mods, path))) {
+      return modificationLoader.getPathModifications();
+    }
+  }
+
+  @Override
+  public List<Modification> getPathModifications(TsFileResource 
tsFileResource, PartialPath path) {
+    if (!checkIfModificationExists(tsFileResource)) {
+      return Collections.emptyList();
+    }
+    if (memoryReservationManager == null) {
+      return super.getPathModifications(tsFileResource, path);
+    }
+    try (QueryModificationLoader modificationLoader =
+        getQueryModificationLoader(
+            tsFileResource,
+            modification -> modification.getPath().overlapWith(path),
+            mods -> getPathModifications(mods, path))) {
+      return modificationLoader.getPathModifications();
+    } catch (IllegalPathException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  private QueryModificationLoader getQueryModificationLoader(
+      TsFileResource tsFileResource,
+      Predicate<Modification> fallbackModificationMatcher,
+      QueryModificationLoader.ModsTreeMatcher modsTreeMatcher) {
+    return new QueryModificationLoader(
+        tsFileResource,
+        memoryReservationManager,
+        CONFIG.getModsCacheSizeLimitPerFI(),
+        MODS_MEMORY_ESTIMATE_READ_INTERVAL,
+        fileModCache,
+        cachedModEntriesSize,
+        fallbackModificationMatcher,
+        modsTreeMatcher);
+  }
+
   @Override
   protected boolean checkIfModificationExists(TsFileResource tsFileResource) {
     if (isSingleSourcePath()) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryModificationLoader.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryModificationLoader.java
new file mode 100644
index 00000000000..26356df58b8
--- /dev/null
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryModificationLoader.java
@@ -0,0 +1,306 @@
+/*
+ * 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.queryengine.execution.fragment;
+
+import org.apache.iotdb.commons.exception.IllegalPathException;
+import org.apache.iotdb.commons.path.PatternTreeMap;
+import org.apache.iotdb.db.queryengine.exception.MemoryNotEnoughException;
+import 
org.apache.iotdb.db.queryengine.plan.planner.memory.MemoryReservationManager;
+import org.apache.iotdb.db.storageengine.dataregion.modification.Modification;
+import 
org.apache.iotdb.db.storageengine.dataregion.modification.ModificationFile;
+import 
org.apache.iotdb.db.storageengine.dataregion.modification.io.ModificationIterator;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileID;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+import org.apache.iotdb.db.utils.datastructure.PatternTreeMapFactory;
+
+import org.apache.tsfile.utils.RamUsageEstimator;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
+
+class QueryModificationLoader implements AutoCloseable {
+
+  private final TsFileResource resource;
+  private final MemoryReservationManager memoryReservationManager;
+  private final long modsCacheSizeLimitPerFI;
+  private final int modsMemoryEstimateReadInterval;
+  private final Map<TsFileID, PatternTreeMap<Modification, 
PatternTreeMapFactory.ModsSerializer>>
+      fileModCache;
+  private final AtomicLong cachedModEntriesSize;
+  private final Predicate<Modification> modificationMatcher;
+  private final ModsTreeMatcher modsTreeMatcher;
+
+  private ModificationIterator currentIterator;
+
+  QueryModificationLoader(
+      TsFileResource resource,
+      MemoryReservationManager memoryReservationManager,
+      long modsCacheSizeLimitPerFI,
+      int modsMemoryEstimateReadInterval,
+      Map<TsFileID, PatternTreeMap<Modification, 
PatternTreeMapFactory.ModsSerializer>>
+          fileModCache,
+      AtomicLong cachedModEntriesSize,
+      Predicate<Modification> modificationMatcher,
+      ModsTreeMatcher modsTreeMatcher) {
+    this.resource = resource;
+    this.memoryReservationManager = memoryReservationManager;
+    this.modsCacheSizeLimitPerFI = modsCacheSizeLimitPerFI;
+    this.modsMemoryEstimateReadInterval = modsMemoryEstimateReadInterval;
+    this.fileModCache = fileModCache;
+    this.cachedModEntriesSize = cachedModEntriesSize;
+    this.modificationMatcher = modificationMatcher;
+    this.modsTreeMatcher = modsTreeMatcher;
+  }
+
+  List<Modification> getPathModifications() throws IllegalPathException {
+    AtomicReference<LoadModsResult> loadedResult = new AtomicReference<>();
+    PatternTreeMap<Modification, PatternTreeMapFactory.ModsSerializer> 
cachedMods =
+        fileModCache.computeIfAbsent(
+            resource.getTsFileID(), ignored -> 
loadAllModificationsForCache(loadedResult));
+    if (cachedMods != null) {
+      return modsTreeMatcher.match(cachedMods);
+    }
+
+    LoadModsResult result = loadedResult.get();
+    try {
+      if (result.loadedAllModEntries) {
+        return fallbackByMatchLoadedPatternTree(result);
+      } else {
+        return fallbackByMatchedScan(result);
+      }
+    } finally {
+      close();
+    }
+  }
+
+  private PatternTreeMap<Modification, PatternTreeMapFactory.ModsSerializer>
+      loadAllModificationsForCache(AtomicReference<LoadModsResult> 
loadedResult) {
+    LoadModsResult result = loadAllModificationsWithQuotaControl();
+    loadedResult.set(result);
+    if (!result.cacheable) {
+      return null;
+    }
+
+    closeCurrentIterator();
+    return result.mods;
+  }
+
+  private LoadModsResult loadAllModificationsWithQuotaControl() {
+    PatternTreeMap<Modification, PatternTreeMapFactory.ModsSerializer> 
modifications =
+        PatternTreeMapFactory.getModsPatternTreeMap();
+    LoadModsResult result = new LoadModsResult(modifications);
+    if (resource.getModFile().getSize() > getRemainingCacheQuota()) {
+      currentIterator = resource.getModFile().getModificationsIter();
+      result.loadedAllModEntries = false;
+      result.cacheable = false;
+      return result;
+    }
+
+    currentIterator = resource.getModFile().getModificationsIter();
+
+    int appendedModCount = 0;
+    boolean estimatedAfterLastAppend = false;
+
+    while (currentIterator.hasNext()) {
+      Modification modification = currentIterator.next();
+      modifications.append(modification.getPath(), modification);
+      appendedModCount++;
+      estimatedAfterLastAppend = false;
+
+      if (appendedModCount % modsMemoryEstimateReadInterval == 0) {
+        if (!tryEstimateAndReserveTreeMemory(result)) {
+          result.loadedAllModEntries = false;
+          result.cacheable = false;
+          return result;
+        }
+        estimatedAfterLastAppend = true;
+      }
+    }
+
+    if (!estimatedAfterLastAppend) {
+      result.cacheable = tryEstimateAndReserveTreeMemory(result);
+    } else {
+      result.cacheable = true;
+    }
+
+    result.loadedAllModEntries = true;
+    return result;
+  }
+
+  private boolean tryEstimateAndReserveTreeMemory(LoadModsResult result) {
+    long currentEstimatedSize = estimateModsTreeMemory(result.mods);
+    long delta = currentEstimatedSize - result.reservedTreeMemoryBytes;
+    if (delta < 0) {
+      throw new IllegalStateException(
+          String.format(
+              "Estimated mods tree size decreased from %d to %d for TsFile 
%s.",
+              result.reservedTreeMemoryBytes, currentEstimatedSize, resource));
+    }
+    if (delta == 0) {
+      return true;
+    }
+
+    if (!tryClaimCacheQuota(delta)) {
+      return false;
+    }
+    result.cacheQuotaBytes += delta;
+
+    try {
+      memoryReservationManager.reserveMemoryImmediately(delta);
+    } catch (MemoryNotEnoughException e) {
+      return false;
+    }
+
+    result.reservedTreeMemoryBytes = currentEstimatedSize;
+    return true;
+  }
+
+  private boolean tryClaimCacheQuota(long delta) {
+    if (delta <= 0) {
+      return true;
+    }
+
+    long alreadyUsedMemoryForCachedModEntries = cachedModEntriesSize.get();
+    while (alreadyUsedMemoryForCachedModEntries + delta <= 
modsCacheSizeLimitPerFI) {
+      if (cachedModEntriesSize.compareAndSet(
+          alreadyUsedMemoryForCachedModEntries, 
alreadyUsedMemoryForCachedModEntries + delta)) {
+        return true;
+      }
+      alreadyUsedMemoryForCachedModEntries = cachedModEntriesSize.get();
+    }
+    return false;
+  }
+
+  private long getRemainingCacheQuota() {
+    return modsCacheSizeLimitPerFI - cachedModEntriesSize.get();
+  }
+
+  private List<Modification> fallbackByMatchedScan(LoadModsResult partialTree)
+      throws IllegalPathException {
+    List<Modification> matchedMods = matchLoadedTreeAndRelease(partialTree);
+    long reservedMatchedModsMemoryBytes = 
reserveMatchedModsMemory(matchedMods);
+    int matchedModCount = matchedMods.size();
+
+    while (currentIterator.hasNext()) {
+      Modification modification = currentIterator.next();
+      if (modificationMatcher.test(modification)) {
+        matchedMods.add(modification);
+        matchedModCount++;
+        if (matchedModCount % modsMemoryEstimateReadInterval == 0) {
+          reservedMatchedModsMemoryBytes =
+              reserveMatchedModsMemoryIncrementally(matchedMods, 
reservedMatchedModsMemoryBytes);
+        }
+      }
+    }
+
+    List<Modification> sortedAndMergedMods = 
ModificationFile.sortAndMerge(matchedMods);
+    adjustMatchedModsMemoryReservation(sortedAndMergedMods, 
reservedMatchedModsMemoryBytes);
+    return sortedAndMergedMods;
+  }
+
+  private List<Modification> fallbackByMatchLoadedPatternTree(LoadModsResult 
loadedTree)
+      throws IllegalPathException {
+    List<Modification> matchedMods = matchLoadedTreeAndRelease(loadedTree);
+    reserveMatchedModsMemory(matchedMods);
+    return matchedMods;
+  }
+
+  private List<Modification> matchLoadedTreeAndRelease(LoadModsResult 
loadedTree)
+      throws IllegalPathException {
+    try {
+      return new ArrayList<>(modsTreeMatcher.match(loadedTree.mods));
+    } finally {
+      loadedTree.mods = null;
+      cachedModEntriesSize.addAndGet(-loadedTree.cacheQuotaBytes);
+      loadedTree.cacheQuotaBytes = 0;
+      
memoryReservationManager.releaseMemoryCumulatively(loadedTree.reservedTreeMemoryBytes);
+      loadedTree.reservedTreeMemoryBytes = 0;
+    }
+  }
+
+  private long reserveMatchedModsMemory(List<Modification> matchedMods) {
+    long estimatedSize = RamUsageEstimator.sizeOfArrayList(matchedMods);
+    memoryReservationManager.reserveMemoryCumulatively(estimatedSize);
+    return estimatedSize;
+  }
+
+  private long reserveMatchedModsMemoryIncrementally(
+      List<Modification> matchedMods, long reservedMatchedModsMemoryBytes) {
+    long currentEstimatedSize = RamUsageEstimator.sizeOfArrayList(matchedMods);
+    long delta = currentEstimatedSize - reservedMatchedModsMemoryBytes;
+    memoryReservationManager.reserveMemoryCumulatively(delta);
+    return currentEstimatedSize;
+  }
+
+  private void adjustMatchedModsMemoryReservation(
+      List<Modification> matchedMods, long reservedMatchedModsMemoryBytes) {
+    long currentEstimatedSize = RamUsageEstimator.sizeOfArrayList(matchedMods);
+    long delta = currentEstimatedSize - reservedMatchedModsMemoryBytes;
+    if (delta >= 0) {
+      memoryReservationManager.reserveMemoryCumulatively(delta);
+    } else {
+      memoryReservationManager.releaseMemoryCumulatively(-delta);
+    }
+  }
+
+  private long estimateModsTreeMemory(
+      PatternTreeMap<Modification, PatternTreeMapFactory.ModsSerializer> 
modifications) {
+    return RamUsageEstimator.sizeOfObject(modifications)
+        + RamUsageEstimator.SHALLOW_SIZE_OF_CONCURRENT_HASHMAP_ENTRY;
+  }
+
+  @Override
+  public void close() {
+    closeCurrentIterator();
+  }
+
+  private void closeCurrentIterator() {
+    if (currentIterator != null) {
+      currentIterator.close();
+      currentIterator = null;
+    }
+  }
+
+  private static class LoadModsResult {
+
+    private PatternTreeMap<Modification, PatternTreeMapFactory.ModsSerializer> 
mods;
+    private long cacheQuotaBytes;
+    private long reservedTreeMemoryBytes;
+    private boolean loadedAllModEntries;
+    private boolean cacheable;
+
+    private LoadModsResult(
+        PatternTreeMap<Modification, PatternTreeMapFactory.ModsSerializer> 
mods) {
+      this.mods = mods;
+    }
+  }
+
+  @FunctionalInterface
+  interface ModsTreeMatcher {
+
+    List<Modification> match(
+        PatternTreeMap<Modification, PatternTreeMapFactory.ModsSerializer> 
modsTree)
+        throws IllegalPathException;
+  }
+}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/FakedMemoryReservationManager.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/FakedMemoryReservationManager.java
index 1742a8070b3..2ea39471f40 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/FakedMemoryReservationManager.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/FakedMemoryReservationManager.java
@@ -29,6 +29,9 @@ public class FakedMemoryReservationManager implements 
MemoryReservationManager {
   @Override
   public void reserveMemoryImmediately() {}
 
+  @Override
+  public void reserveMemoryImmediately(final long size) {}
+
   @Override
   public void releaseMemoryCumulatively(long size) {}
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/MemoryReservationManager.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/MemoryReservationManager.java
index 9a9036b1d97..1013167ac68 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/MemoryReservationManager.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/memory/MemoryReservationManager.java
@@ -33,6 +33,14 @@ public interface MemoryReservationManager {
   /** Reserve memory for the accumulated memory size immediately. */
   void reserveMemoryImmediately();
 
+  /**
+   * Reserve memory for the given size immediately without changing the 
accumulated pending
+   * reservation size maintained by this manager.
+   *
+   * @param size the size of memory to reserve immediately
+   */
+  void reserveMemoryImmediately(final long size);
+
   /**
    * Release memory for the given size.
    *
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/ModificationFile.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/ModificationFile.java
index bab214c5974..4af196641f2 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/ModificationFile.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/ModificationFile.java
@@ -21,6 +21,7 @@ package 
org.apache.iotdb.db.storageengine.dataregion.modification;
 
 import org.apache.iotdb.commons.utils.FileUtils;
 import 
org.apache.iotdb.db.storageengine.dataregion.modification.io.LocalTextModificationAccessor;
+import 
org.apache.iotdb.db.storageengine.dataregion.modification.io.ModificationIterator;
 import 
org.apache.iotdb.db.storageengine.dataregion.modification.io.ModificationReader;
 import 
org.apache.iotdb.db.storageengine.dataregion.modification.io.ModificationWriter;
 import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
@@ -139,8 +140,8 @@ public class ModificationFile implements AutoCloseable {
     }
   }
 
-  public Iterable<Modification> getModificationsIter() {
-    return reader::getModificationIterator;
+  public ModificationIterator getModificationsIter() {
+    return reader.getModificationIterator();
   }
 
   public String getFilePath() {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/LocalTextModificationAccessor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/LocalTextModificationAccessor.java
index eccc781d823..a08b759385c 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/LocalTextModificationAccessor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/LocalTextModificationAccessor.java
@@ -39,7 +39,6 @@ import java.io.RandomAccessFile;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Iterator;
 import java.util.List;
 import java.util.NoSuchElementException;
 
@@ -70,68 +69,106 @@ public class LocalTextModificationAccessor
   @Override
   public Collection<Modification> read() {
     List<Modification> result = new ArrayList<>();
-    Iterator<Modification> iterator = getModificationIterator();
-    while (iterator.hasNext()) {
-      result.add(iterator.next());
+    ModificationIterator iterator = getModificationIterator();
+    try {
+      while (iterator.hasNext()) {
+        result.add(iterator.next());
+      }
+      return result;
+    } finally {
+      iterator.close();
     }
-    return result;
   }
 
-  // we need to hold the reader for the Iterator, cannot use auto close or 
close in finally block
+  // The iterator owns the reader; callers must close it when they no longer 
need the iterator.
   @SuppressWarnings("java:S2095")
   @Override
-  public Iterator<Modification> getModificationIterator() {
+  public ModificationIterator getModificationIterator() {
     File file = FSFactoryProducer.getFSFactory().getFile(filePath);
     final BufferedReader reader;
     try {
       reader = new BufferedReader(new FileReader(file));
     } catch (FileNotFoundException e) {
       logger.debug(NO_MODIFICATION_MSG, file);
+      return new EmptyModificationIterator();
+    }
 
-      // return empty iterator
-      return new Iterator<Modification>() {
-        @Override
-        public boolean hasNext() {
-          return false;
-        }
+    return new FileModificationIterator(reader);
+  }
 
-        @Override
-        public Modification next() {
-          throw new NoSuchElementException();
-        }
-      };
+  private class FileModificationIterator implements ModificationIterator {
+
+    private final BufferedReader reader;
+    private final Modification[] cachedModification = new Modification[1];
+    private boolean closed = false;
+
+    private FileModificationIterator(BufferedReader reader) {
+      this.reader = reader;
     }
 
-    final Modification[] cachedModification = new Modification[1];
-    return new Iterator<Modification>() {
-      @Override
-      public boolean hasNext() {
-        try {
-          if (cachedModification[0] == null) {
-            String line = reader.readLine();
-            if (line == null) {
-              reader.close();
-              return false;
-            } else {
-              return decodeModificationAndCache(reader, cachedModification, 
line);
-            }
+    @Override
+    public boolean hasNext() {
+      if (closed) {
+        return false;
+      }
+      try {
+        if (cachedModification[0] == null) {
+          String line = reader.readLine();
+          if (line == null) {
+            close();
+            return false;
+          }
+          if (!decodeModificationAndCache(reader, cachedModification, line)) {
+            close();
+            return false;
           }
-        } catch (IOException e) {
-          logger.warn("An error occurred when reading modifications", e);
         }
-        return true;
+      } catch (IOException e) {
+        logger.warn("An error occurred when reading modifications", e);
+        close();
+        return false;
       }
+      return true;
+    }
 
-      @Override
-      public Modification next() {
-        if (cachedModification[0] == null) {
-          throw new NoSuchElementException();
-        }
-        Modification result = cachedModification[0];
-        cachedModification[0] = null;
-        return result;
+    @Override
+    public Modification next() {
+      if (cachedModification[0] == null) {
+        throw new NoSuchElementException();
       }
-    };
+      Modification result = cachedModification[0];
+      cachedModification[0] = null;
+      return result;
+    }
+
+    @Override
+    public void close() {
+      if (closed) {
+        return;
+      }
+      closed = true;
+      try {
+        reader.close();
+      } catch (IOException e) {
+        logger.warn("An error occurred when closing modification reader", e);
+      }
+    }
+  }
+
+  private static class EmptyModificationIterator implements 
ModificationIterator {
+
+    @Override
+    public boolean hasNext() {
+      return false;
+    }
+
+    @Override
+    public Modification next() {
+      throw new NoSuchElementException();
+    }
+
+    @Override
+    public void close() {}
   }
 
   private boolean decodeModificationAndCache(
@@ -142,7 +179,6 @@ public class LocalTextModificationAccessor
     } catch (IOException e) {
       logger.warn("An error occurred when decode line-[{}] to modification", 
line);
       cachedModification[0] = null;
-      reader.close();
       return false;
     }
   }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/ModificationReader.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/ModificationIterator.java
similarity index 54%
copy from 
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/ModificationReader.java
copy to 
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/ModificationIterator.java
index 2da82b13d34..09ae741cdb6 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/ModificationReader.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/ModificationIterator.java
@@ -21,29 +21,17 @@ package 
org.apache.iotdb.db.storageengine.dataregion.modification.io;
 
 import org.apache.iotdb.db.storageengine.dataregion.modification.Modification;
 
-import java.io.IOException;
-import java.util.Collection;
 import java.util.Iterator;
 
-/** ModificationReader reads all modifications from a persistent medium like 
file system. */
-public interface ModificationReader {
+/** An iterator over modifications that can be closed to release underlying 
resources. */
+public interface ModificationIterator
+    extends Iterator<Modification>, Iterable<Modification>, AutoCloseable {
 
-  /**
-   * Read all modifications from a persistent medium. If the mods file is 
crashed, the redundant
-   * modifications will be truncated until the file is correct.
-   *
-   * @return a list of modifications contained the medium.
-   */
-  Collection<Modification> read();
+  @Override
+  void close();
 
-  /**
-   * Get an iterator over this mod file, others keep consistence with {@link 
#read()}. Please ensure
-   * you have called hasNext() with return of {@code true} before calling 
next().
-   *
-   * @return the modification iterator.
-   */
-  Iterator<Modification> getModificationIterator();
-
-  /** Release resources like streams. */
-  void close() throws IOException;
+  @Override
+  default Iterator<Modification> iterator() {
+    return this;
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/ModificationReader.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/ModificationReader.java
index 2da82b13d34..dd46328db59 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/ModificationReader.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/modification/io/ModificationReader.java
@@ -23,7 +23,6 @@ import 
org.apache.iotdb.db.storageengine.dataregion.modification.Modification;
 
 import java.io.IOException;
 import java.util.Collection;
-import java.util.Iterator;
 
 /** ModificationReader reads all modifications from a persistent medium like 
file system. */
 public interface ModificationReader {
@@ -42,7 +41,7 @@ public interface ModificationReader {
    *
    * @return the modification iterator.
    */
-  Iterator<Modification> getModificationIterator();
+  ModificationIterator getModificationIterator();
 
   /** Release resources like streams. */
   void close() throws IOException;
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryModificationLoaderTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryModificationLoaderTest.java
new file mode 100644
index 00000000000..dc76e4123c9
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryModificationLoaderTest.java
@@ -0,0 +1,372 @@
+/*
+ * 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.queryengine.execution.fragment;
+
+import org.apache.iotdb.commons.exception.IllegalPathException;
+import org.apache.iotdb.commons.path.MeasurementPath;
+import org.apache.iotdb.commons.path.PartialPath;
+import org.apache.iotdb.commons.path.PatternTreeMap;
+import org.apache.iotdb.db.queryengine.exception.MemoryNotEnoughException;
+import 
org.apache.iotdb.db.queryengine.plan.planner.memory.MemoryReservationManager;
+import org.apache.iotdb.db.storageengine.dataregion.modification.Deletion;
+import org.apache.iotdb.db.storageengine.dataregion.modification.Modification;
+import 
org.apache.iotdb.db.storageengine.dataregion.modification.ModificationFile;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileID;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+import 
org.apache.iotdb.db.storageengine.dataregion.tsfile.generator.TsFileNameGenerator;
+import org.apache.iotdb.db.utils.constant.TestConstant;
+import org.apache.iotdb.db.utils.datastructure.PatternTreeMapFactory;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.tsfile.file.metadata.IDeviceID;
+import org.apache.tsfile.file.metadata.PlainDeviceID;
+import org.apache.tsfile.utils.Pair;
+import org.junit.After;
+import org.junit.Test;
+
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class QueryModificationLoaderTest {
+
+  private static final IDeviceID DEVICE_ID = new PlainDeviceID("root.sg.d1");
+
+  private File testDir;
+
+  @After
+  public void tearDown() throws Exception {
+    if (testDir != null && testDir.exists()) {
+      FileUtils.deleteDirectory(testDir);
+    }
+  }
+
+  @Test
+  public void testCacheLoadedModsTreeWhenQuotaEnough() throws Exception {
+    TsFileResource resource = prepareResource("cache");
+    writeMods(resource, deletion("root.sg.d1.s1", 0, 10), 
deletion("root.sg.d2.s1", 20, 30));
+
+    Map<TsFileID, PatternTreeMap<Modification, 
PatternTreeMapFactory.ModsSerializer>> fileModCache =
+        new ConcurrentHashMap<>();
+    AtomicLong cachedModEntriesSize = new AtomicLong();
+    CountingMemoryReservationManager memoryReservationManager =
+        new CountingMemoryReservationManager();
+
+    try (QueryModificationLoader loader =
+        newLoader(
+            resource,
+            Long.MAX_VALUE,
+            fileModCache,
+            cachedModEntriesSize,
+            memoryReservationManager,
+            1)) {
+      List<Modification> result = loader.getPathModifications();
+
+      assertEquals(1, result.size());
+      assertTrue(fileModCache.containsKey(resource.getTsFileID()));
+      assertTrue(cachedModEntriesSize.get() > 0);
+      assertTrue(memoryReservationManager.getReservedBytes() >= 
cachedModEntriesSize.get());
+      assertTrue(memoryReservationManager.getImmediateReservationCount() > 0);
+    }
+  }
+
+  @Test
+  public void 
testFallbackScansModsWhenFileSizeExceedsRemainingQuotaBeforeLoad() throws 
Exception {
+    TsFileResource resource = prepareResource("file-size-precheck-fallback");
+    writeMods(
+        resource,
+        deletion("root.sg.d1.s1", 0, 10),
+        deletion("root.sg.d2.s1", 20, 30),
+        deletion("root.sg.d1.s1", 40, 50));
+
+    Map<TsFileID, PatternTreeMap<Modification, 
PatternTreeMapFactory.ModsSerializer>> fileModCache =
+        new ConcurrentHashMap<>();
+    AtomicLong cachedModEntriesSize = new AtomicLong();
+    CountingMemoryReservationManager memoryReservationManager =
+        new CountingMemoryReservationManager();
+
+    try (QueryModificationLoader loader =
+        newLoader(resource, 1, fileModCache, cachedModEntriesSize, 
memoryReservationManager, 1)) {
+      List<Modification> result = loader.getPathModifications();
+
+      assertEquals(2, result.size());
+      assertFalse(fileModCache.containsKey(resource.getTsFileID()));
+      assertEquals(0, cachedModEntriesSize.get());
+      assertTrue(memoryReservationManager.getReservedBytes() > 0);
+      assertEquals(0, 
memoryReservationManager.getRemainingImmediateFailures());
+      assertEquals(0, memoryReservationManager.getImmediateReservationCount());
+    }
+  }
+
+  @Test
+  public void testFallbackScansRemainingModsWhenEstimatedTreeExceedsQuota() 
throws Exception {
+    TsFileResource resource = prepareResource("estimated-tree-quota-fallback");
+    writeMods(
+        resource,
+        deletion("root.sg.d1.s1", 0, 10),
+        deletion("root.sg.d2.s1", 20, 30),
+        deletion("root.sg.d1.s1", 40, 50));
+
+    Map<TsFileID, PatternTreeMap<Modification, 
PatternTreeMapFactory.ModsSerializer>> fileModCache =
+        new ConcurrentHashMap<>();
+    AtomicLong cachedModEntriesSize = new AtomicLong();
+    CountingMemoryReservationManager memoryReservationManager =
+        new CountingMemoryReservationManager();
+
+    try (QueryModificationLoader loader =
+        newLoader(
+            resource,
+            resource.getModFile().getSize() + 1,
+            fileModCache,
+            cachedModEntriesSize,
+            memoryReservationManager,
+            1)) {
+      List<Modification> result = loader.getPathModifications();
+
+      assertEquals(2, result.size());
+      assertFalse(fileModCache.containsKey(resource.getTsFileID()));
+      assertEquals(0, cachedModEntriesSize.get());
+      assertTrue(memoryReservationManager.getReservedBytes() > 0);
+      assertTrue(memoryReservationManager.getCumulativeReleaseCount() > 0);
+    }
+  }
+
+  @Test
+  public void testFallbackMatchesLoadedTreeWhenFinalReservationFailed() throws 
Exception {
+    TsFileResource resource = prepareResource("final-reserve-failed");
+    writeMods(
+        resource,
+        deletion("root.sg.d1.s1", 0, 10),
+        deletion("root.sg.d2.s1", 20, 30),
+        deletion("root.sg.d1.s1", 40, 50));
+
+    Map<TsFileID, PatternTreeMap<Modification, 
PatternTreeMapFactory.ModsSerializer>> fileModCache =
+        new ConcurrentHashMap<>();
+    AtomicLong cachedModEntriesSize = new AtomicLong();
+    CountingMemoryReservationManager memoryReservationManager =
+        new CountingMemoryReservationManager(1);
+
+    try (QueryModificationLoader loader =
+        newLoader(
+            resource,
+            Long.MAX_VALUE,
+            fileModCache,
+            cachedModEntriesSize,
+            memoryReservationManager,
+            100)) {
+      List<Modification> result = loader.getPathModifications();
+
+      assertEquals(2, result.size());
+      assertFalse(fileModCache.containsKey(resource.getTsFileID()));
+      assertEquals(0, cachedModEntriesSize.get());
+      assertTrue(memoryReservationManager.getReservedBytes() > 0);
+      assertEquals(0, 
memoryReservationManager.getRemainingImmediateFailures());
+      assertEquals(1, memoryReservationManager.getImmediateReservationCount());
+    }
+  }
+
+  @Test
+  public void testFallbackReservesMatchedModsCumulativelyWhenQuotaExceeded() 
throws Exception {
+    TsFileResource resource = prepareResource("fallback-cumulative-reserve");
+    writeMods(
+        resource,
+        deletion("root.sg.d1.s1", 0, 10),
+        deletion("root.sg.d2.s1", 20, 30),
+        deletion("root.sg.d1.s1", 40, 50));
+
+    Map<TsFileID, PatternTreeMap<Modification, 
PatternTreeMapFactory.ModsSerializer>> fileModCache =
+        new ConcurrentHashMap<>();
+    AtomicLong cachedModEntriesSize = new AtomicLong();
+    CountingMemoryReservationManager memoryReservationManager =
+        new CountingMemoryReservationManager(1);
+
+    try (QueryModificationLoader loader =
+        newLoader(resource, 1, fileModCache, cachedModEntriesSize, 
memoryReservationManager, 1)) {
+      List<Modification> result = loader.getPathModifications();
+
+      assertEquals(2, result.size());
+      assertFalse(fileModCache.containsKey(resource.getTsFileID()));
+      assertEquals(0, cachedModEntriesSize.get());
+      assertTrue(memoryReservationManager.getReservedBytes() > 0);
+      assertEquals(1, 
memoryReservationManager.getRemainingImmediateFailures());
+      assertEquals(0, memoryReservationManager.getImmediateReservationCount());
+    }
+  }
+
+  @Test
+  public void testFallbackAdjustsReservedMemoryAfterSortAndMerge() throws 
Exception {
+    TsFileResource resource = prepareResource("fallback-sort-merge-adjust");
+    writeMods(
+        resource,
+        deletion("root.sg.d1.s1", 0, 10),
+        deletion("root.sg.d1.s1", 5, 15),
+        deletion("root.sg.d2.s1", 20, 30));
+
+    Map<TsFileID, PatternTreeMap<Modification, 
PatternTreeMapFactory.ModsSerializer>> fileModCache =
+        new ConcurrentHashMap<>();
+    AtomicLong cachedModEntriesSize = new AtomicLong();
+    CountingMemoryReservationManager memoryReservationManager =
+        new CountingMemoryReservationManager();
+
+    try (QueryModificationLoader loader =
+        newLoader(resource, 1, fileModCache, cachedModEntriesSize, 
memoryReservationManager, 1)) {
+      List<Modification> result = loader.getPathModifications();
+
+      assertEquals(1, result.size());
+      assertFalse(fileModCache.containsKey(resource.getTsFileID()));
+      assertEquals(0, cachedModEntriesSize.get());
+      assertTrue(memoryReservationManager.getReservedBytes() > 0);
+      assertTrue(memoryReservationManager.getCumulativeReleaseCount() > 0);
+    }
+  }
+
+  private QueryModificationLoader newLoader(
+      TsFileResource resource,
+      long modsCacheSizeLimitPerFI,
+      Map<TsFileID, PatternTreeMap<Modification, 
PatternTreeMapFactory.ModsSerializer>>
+          fileModCache,
+      AtomicLong cachedModEntriesSize,
+      MemoryReservationManager memoryReservationManager,
+      int modsMemoryEstimateReadInterval)
+      throws IllegalPathException {
+    QueryContext queryContext = new QueryContext(false);
+    PartialPath queryPath = new PartialPath(DEVICE_ID, "s1");
+    return new QueryModificationLoader(
+        resource,
+        memoryReservationManager,
+        modsCacheSizeLimitPerFI,
+        modsMemoryEstimateReadInterval,
+        fileModCache,
+        cachedModEntriesSize,
+        modification -> modification.getPath().overlapWith(queryPath),
+        modsTree -> queryContext.getPathModifications(modsTree, queryPath));
+  }
+
+  private TsFileResource prepareResource(String name) {
+    testDir = new File(TestConstant.BASE_OUTPUT_PATH, 
"QueryModificationLoaderTest-" + name);
+    testDir.mkdirs();
+    File tsFile =
+        new 
File(TsFileNameGenerator.generateNewTsFilePath(testDir.getAbsolutePath(), 1, 1, 
0, 0));
+    return new TsFileResource(tsFile);
+  }
+
+  private static Deletion deletion(String path, long startTime, long endTime)
+      throws IllegalPathException {
+    return new Deletion(new MeasurementPath(path), 0, startTime, endTime);
+  }
+
+  private void writeMods(TsFileResource resource, Deletion... modifications) 
throws Exception {
+    try (ModificationFile modificationFile = resource.getModFile()) {
+      for (Deletion modification : modifications) {
+        modificationFile.write(modification);
+      }
+    }
+  }
+
+  private static class CountingMemoryReservationManager implements 
MemoryReservationManager {
+
+    private long reservedBytes;
+    private int remainingImmediateFailures;
+    private int immediateReservationCount;
+    private int cumulativeReleaseCount;
+
+    private CountingMemoryReservationManager() {}
+
+    private CountingMemoryReservationManager(int remainingImmediateFailures) {
+      this.remainingImmediateFailures = remainingImmediateFailures;
+    }
+
+    @Override
+    public void reserveMemoryCumulatively(long size) {
+      reservedBytes += size;
+    }
+
+    @Override
+    public void reserveMemoryImmediately() {
+      immediateReservationCount++;
+      if (remainingImmediateFailures > 0) {
+        remainingImmediateFailures--;
+        throw new MemoryNotEnoughException("Mock memory reservation failure.");
+      }
+    }
+
+    @Override
+    public void reserveMemoryImmediately(long size) {
+      immediateReservationCount++;
+      if (remainingImmediateFailures > 0) {
+        remainingImmediateFailures--;
+        throw new MemoryNotEnoughException("Mock memory reservation failure.");
+      }
+      reservedBytes += size;
+    }
+
+    @Override
+    public void releaseMemoryCumulatively(long size) {
+      cumulativeReleaseCount++;
+      reservedBytes -= size;
+    }
+
+    @Override
+    public void releaseMemoryImmediately(long size) {
+      reservedBytes -= size;
+    }
+
+    @Override
+    public void releaseAllReservedMemory() {
+      reservedBytes = 0;
+    }
+
+    @Override
+    public Pair<Long, Long> releaseMemoryVirtually(long size) {
+      reservedBytes -= size;
+      return new Pair<>(size, 0L);
+    }
+
+    @Override
+    public void reserveMemoryVirtually(long bytesToBeReserved, long 
bytesAlreadyReserved) {
+      reservedBytes += bytesToBeReserved + bytesAlreadyReserved;
+    }
+
+    @Override
+    public void setHighestPriority(boolean isHighestPriority) {}
+
+    private long getReservedBytes() {
+      return reservedBytes;
+    }
+
+    private int getRemainingImmediateFailures() {
+      return remainingImmediateFailures;
+    }
+
+    private int getImmediateReservationCount() {
+      return immediateReservationCount;
+    }
+
+    private int getCumulativeReleaseCount() {
+      return cumulativeReleaseCount;
+    }
+  }
+}

Reply via email to