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

JackieTien97 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 48233ea92dd [Fix] Isolate FileReaderManager reader I/O with per-file 
locks (#18788)
48233ea92dd is described below

commit 48233ea92dde92bbc13b0466c2b4ed0173676438
Author: Jackie Tien <[email protected]>
AuthorDate: Fri Oct 9 10:23:39 2026 +0800

    [Fix] Isolate FileReaderManager reader I/O with per-file locks (#18788)
---
 .../iotdb/db/i18n/StorageEngineMessages.java       |   6 +-
 .../iotdb/db/i18n/StorageEngineMessages.java       |   6 +-
 .../fragment/FakedFragmentInstanceContext.java     |  84 +-
 .../fragment/FragmentInstanceContext.java          | 181 +++--
 .../dataregion/read/control/FileReaderManager.java | 698 +++++++++++------
 .../dataregion/tsfile/TsFileLock.java              |  27 +-
 .../fragment/FileReaderReferenceLifecycleTest.java | 189 +++++
 .../RegionQueryInitializationLockTest.java         | 280 +++++++
 .../fragment/RegionScanTimeSliceTest.java          |  96 +++
 .../buffer/TimeSeriesMetadataCacheTest.java        |   4 +-
 .../control/FileReaderManagerConcurrencyTest.java  | 871 +++++++++++++++++++++
 .../dataregion/tsfile/TsFileLockTest.java          | 108 +++
 12 files changed, 2221 insertions(+), 329 deletions(-)

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 032db3e9bb1..2a440d6d32d 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
@@ -1495,8 +1495,10 @@ public final class StorageEngineMessages {
       "Current chunk %s cannot be aligned with time chunk: %s, all time chunk 
in first batch is %s";
   public static final String WAL_NODE_CLOSED_FMT = "wal node-%s has been 
closed";
   public static final String BROKEN_WAL_FILE_FMT = "Broken wal file %s, size 
%d";
-  public static final String TSFILE_READER_CLOSED_BECAUSE_NO_REFERENCE =
-      "{} TsFileReader is closed because of no reference.";
+  public static final String 
LOG_READER_FOR_CLOSED_TSFILE_ARG_IS_CLOSED_BECAUSE_ITS_REFERENCE_COUNT_REACHED_ZERO_C3B71A85
 =
+      "Reader for closed TsFile {} is closed because its reference count 
reached zero.";
+  public static final String 
LOG_READER_FOR_UNCLOSED_TSFILE_ARG_IS_CLOSED_BECAUSE_ITS_REFERENCE_COUNT_REACHED_ZERO_088BDEF8
 =
+      "Reader for unclosed TsFile {} is closed because its reference count 
reached zero.";
   public static final String CLOSED_TSFILE_READER_CLOSED =
       "{} closedTsFileReader is closed.";
   public static final String UNCLOSED_TSFILE_READER_CLOSED =
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 43ebfbd35ab..b55c248397e 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
@@ -1424,8 +1424,10 @@ public final class StorageEngineMessages {
       "当前 chunk %s 无法与 time chunk:%s 对齐,第一批中的所有 time chunk 为 %s";
   public static final String WAL_NODE_CLOSED_FMT = "wal node-%s 已关闭";
   public static final String BROKEN_WAL_FILE_FMT = "WAL 文件 %s 损坏,大小为 %d";
-  public static final String TSFILE_READER_CLOSED_BECAUSE_NO_REFERENCE =
-      "{} TsFileReader 因没有引用已关闭。";
+  public static final String 
LOG_READER_FOR_CLOSED_TSFILE_ARG_IS_CLOSED_BECAUSE_ITS_REFERENCE_COUNT_REACHED_ZERO_C3B71A85
 =
+      "已封口 TsFile {} 的 reader 因引用计数归零而关闭。";
+  public static final String 
LOG_READER_FOR_UNCLOSED_TSFILE_ARG_IS_CLOSED_BECAUSE_ITS_REFERENCE_COUNT_REACHED_ZERO_088BDEF8
 =
+      "未封口 TsFile {} 的 reader 因引用计数归零而关闭。";
   public static final String CLOSED_TSFILE_READER_CLOSED =
       "{} closedTsFileReader 已关闭。";
   public static final String UNCLOSED_TSFILE_READER_CLOSED =
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FakedFragmentInstanceContext.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FakedFragmentInstanceContext.java
index 99e95dbf26b..c2f29ed1ab8 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FakedFragmentInstanceContext.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FakedFragmentInstanceContext.java
@@ -24,16 +24,21 @@ import org.apache.iotdb.commons.path.IFullPath;
 import 
org.apache.iotdb.db.queryengine.plan.planner.memory.FakedMemoryReservationManager;
 import org.apache.iotdb.db.storageengine.dataregion.DataRegion;
 import org.apache.iotdb.db.storageengine.dataregion.read.QueryDataSource;
-import 
org.apache.iotdb.db.storageengine.dataregion.read.control.FileReaderManager;
 import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
 
 import org.apache.tsfile.read.filter.basic.Filter;
 
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 public class FakedFragmentInstanceContext extends FragmentInstanceContext {
 
+  // Retain the registration-time state: an unclosed file may be sealed before 
the query finishes.
+  private final Set<TsFileResource> closedResources = new HashSet<>();
+  private final Set<TsFileResource> unclosedResources = new HashSet<>();
+
   public FakedFragmentInstanceContext(Filter timeFilter, DataRegion 
dataRegion) {
     super(0, new FakedMemoryReservationManager(), timeFilter, dataRegion, 
false, false);
   }
@@ -50,52 +55,55 @@ public class FakedFragmentInstanceContext extends 
FragmentInstanceContext {
 
     dataRegion.tryReadLock(Long.MAX_VALUE);
     try {
-      this.sharedQueryDataSource =
-          dataRegion.query(
-              Collections.singletonList(sourcePath),
-              sourcePath.getDeviceId(),
-              this,
-              getGlobalTimeFilter(),
-              null,
-              Long.MAX_VALUE);
+      try {
+        this.sharedQueryDataSource =
+            dataRegion.query(
+                Collections.singletonList(sourcePath),
+                sourcePath.getDeviceId(),
+                this,
+                getGlobalTimeFilter(),
+                null,
+                Long.MAX_VALUE);
 
-      // used files should be added before mergeLock is unlocked, or they may 
be deleted by
-      // running merge
-      if (sharedQueryDataSource != null) {
-        ((QueryDataSource) sharedQueryDataSource).setSingleDevice(true);
-        List<TsFileResource> tsFileList =
-            ((QueryDataSource) sharedQueryDataSource).getSeqResources();
-        if (tsFileList != null) {
-          for (TsFileResource tsFile : tsFileList) {
-            
FileReaderManager.getInstance().increaseFileReaderReference(tsFile, 
tsFile.isClosed());
+        // used files should be added before mergeLock is unlocked, or they 
may be deleted by
+        // running merge
+        if (sharedQueryDataSource != null) {
+          ((QueryDataSource) sharedQueryDataSource).setSingleDevice(true);
+          List<TsFileResource> tsFileList =
+              ((QueryDataSource) sharedQueryDataSource).getSeqResources();
+          if (tsFileList != null) {
+            for (TsFileResource tsFile : tsFileList) {
+              addFilePathToMap(tsFile, tsFile.isClosed(), closedResources, 
unclosedResources);
+            }
           }
-        }
-        tsFileList = ((QueryDataSource) 
sharedQueryDataSource).getUnseqResources();
-        if (tsFileList != null) {
-          for (TsFileResource tsFile : tsFileList) {
-            
FileReaderManager.getInstance().increaseFileReaderReference(tsFile, 
tsFile.isClosed());
+          tsFileList = ((QueryDataSource) 
sharedQueryDataSource).getUnseqResources();
+          if (tsFileList != null) {
+            for (TsFileResource tsFile : tsFileList) {
+              addFilePathToMap(tsFile, tsFile.isClosed(), closedResources, 
unclosedResources);
+            }
           }
         }
+      } finally {
+        dataRegion.readUnlock();
       }
-    } finally {
-      dataRegion.readUnlock();
+    } catch (RuntimeException | Error e) {
+      // Rollback can close readers or wait for another close; never retain 
the region lock here.
+      try {
+        releaseSharedQueryDataSource();
+      } catch (RuntimeException | Error rollbackFailure) {
+        if (rollbackFailure != e) {
+          e.addSuppressed(rollbackFailure);
+        }
+      }
+      throw e;
     }
   }
 
   public void releaseSharedQueryDataSource() {
-    if (sharedQueryDataSource != null) {
-      List<TsFileResource> tsFileList = ((QueryDataSource) 
sharedQueryDataSource).getSeqResources();
-      if (tsFileList != null) {
-        for (TsFileResource tsFile : tsFileList) {
-          FileReaderManager.getInstance().decreaseFileReaderReference(tsFile, 
tsFile.isClosed());
-        }
-      }
-      tsFileList = ((QueryDataSource) 
sharedQueryDataSource).getUnseqResources();
-      if (tsFileList != null) {
-        for (TsFileResource tsFile : tsFileList) {
-          FileReaderManager.getInstance().decreaseFileReaderReference(tsFile, 
tsFile.isClosed());
-        }
-      }
+    try (DeferredFileReaderReleases releases = new 
DeferredFileReaderReleases()) {
+      releases.addAll(closedResources, true);
+      releases.addAll(unclosedResources, false);
+    } finally {
       sharedQueryDataSource = null;
     }
   }
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 c76b15b0f0f..e46603cdf79 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
@@ -76,6 +76,7 @@ import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -87,6 +88,7 @@ import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
@@ -774,16 +776,16 @@ public class FragmentInstanceContext extends QueryContext 
{
   public boolean initQueryDataSource(List<IFullPath> sourcePaths) throws 
QueryProcessException {
     return initializeQueryDataSource(
         sourcePaths,
-        () -> {
+        deferredReleases -> {
           sharedQueryDataSource = EMPTY_QUERY_DATA_SOURCE;
           return true;
         },
         () -> false,
-        dataSource -> {
+        (dataSource, deferredReleases) -> {
           sharedQueryDataSource = dataSource;
           closedFilePaths = new HashSet<>();
           unClosedFilePaths = new HashSet<>();
-          addUsedFilesForQuery(dataSource, closedFilePaths, unClosedFilePaths);
+          addUsedFilesForQuery(dataSource, closedFilePaths, unClosedFilePaths, 
deferredReleases);
           return true;
         });
   }
@@ -792,7 +794,8 @@ public class FragmentInstanceContext extends QueryContext {
       throws QueryProcessException {
     return initializeQueryDataSource(
         sourcePaths,
-        () -> createBatchQueryDataSourceLease(EMPTY_QUERY_DATA_SOURCE),
+        deferredReleases ->
+            createBatchQueryDataSourceLease(EMPTY_QUERY_DATA_SOURCE, 
deferredReleases),
         () -> {
           recordQueryDataSourceRetry();
           return null;
@@ -802,14 +805,14 @@ public class FragmentInstanceContext extends QueryContext 
{
 
   private <T> T initializeQueryDataSource(
       List<IFullPath> sourcePaths,
-      Supplier<T> emptyResultSupplier,
+      Function<DeferredFileReaderReleases, T> emptyResultSupplier,
       Supplier<T> unfinishedResultSupplier,
-      Function<QueryDataSource, T> initializer)
+      BiFunction<QueryDataSource, DeferredFileReaderReleases, T> initializer)
       throws QueryProcessException {
     long startTime = System.nanoTime();
-    try {
+    try (DeferredFileReaderReleases deferredReleases = new 
DeferredFileReaderReleases()) {
       if (sourcePaths == null || sourcePaths.isEmpty()) {
-        return emptyResultSupplier.get();
+        return emptyResultSupplier.apply(deferredReleases);
       }
 
       IDeviceID singleDeviceId = findSingleDeviceId(sourcePaths);
@@ -843,7 +846,7 @@ public class FragmentInstanceContext extends QueryContext {
         dataSource.setSingleDevice(singleDeviceId != null);
         // The initializer retains referenced files while the DataRegion read 
lock is held;
         // otherwise a concurrent merge may delete them after the lock is 
released.
-        return initializer.apply(dataSource);
+        return initializer.apply(dataSource, deferredReleases);
       } finally {
         dataRegion.readUnlock();
       }
@@ -869,14 +872,23 @@ public class FragmentInstanceContext extends QueryContext 
{
     return selectedDeviceIdSet.iterator().next();
   }
 
-  private QueryDataSourceLease createBatchQueryDataSourceLease(QueryDataSource 
dataSource) {
+  private QueryDataSourceLease createBatchQueryDataSourceLease(
+      QueryDataSource dataSource, DeferredFileReaderReleases deferredReleases) 
{
     Set<TsFileResource> closedResources = new HashSet<>();
     Set<TsFileResource> unclosedResources = new HashSet<>();
-    addUsedFilesForQuery(dataSource, closedResources, unclosedResources);
-    QueryDataSourceLease lease =
-        new QueryDataSourceLease(dataSource, closedResources, 
unclosedResources, this);
-    batchQueryDataSourceLeases.add(lease);
-    return lease;
+    try {
+      addUsedFilesForQuery(dataSource, closedResources, unclosedResources, 
deferredReleases);
+      QueryDataSourceLease lease =
+          new QueryDataSourceLease(dataSource, closedResources, 
unclosedResources, this);
+      batchQueryDataSourceLeases.add(lease);
+      return lease;
+    } catch (RuntimeException | Error e) {
+      // No lease owns a partially initialized batch. Release its retained 
files after region
+      // unlock.
+      deferredReleases.addAll(closedResources, true);
+      deferredReleases.addAll(unclosedResources, false);
+      throw e;
+    }
   }
 
   public boolean initRegionScanQueryDataSource(Map<IDeviceID, DeviceContext> 
devicePathsToContext) {
@@ -885,8 +897,11 @@ public class FragmentInstanceContext extends QueryContext {
       return true;
     }
 
-    long waitForLockTime = COMMON_CONFIG.getDriverTaskExecutionTimeSliceInMs();
-    if (dataRegion.tryReadLock(waitForLockTime)) {
+    try (DeferredFileReaderReleases deferredReleases = new 
DeferredFileReaderReleases()) {
+      long waitForLockTime = 
COMMON_CONFIG.getDriverTaskExecutionTimeSliceInMs();
+      if (!dataRegion.tryReadLock(waitForLockTime)) {
+        return false;
+      }
       try {
         // minus already consumed time
         waitForLockTime -= (System.nanoTime() - startTime) / 1_000_000;
@@ -906,19 +921,18 @@ public class FragmentInstanceContext extends QueryContext 
{
         if (sharedQueryDataSource != null) {
           closedFilePaths = new HashSet<>();
           unClosedFilePaths = new HashSet<>();
-          addUsedFilesForRegionQuery((QueryDataSourceForRegionScan) 
sharedQueryDataSource);
+          addUsedFilesForRegionQuery(
+              (QueryDataSourceForRegionScan) sharedQueryDataSource, 
deferredReleases);
           return true;
         } else {
           // failed to acquire lock within the specific time
           return false;
         }
       } finally {
-        addInitQueryDataSourceCost(System.nanoTime() - startTime);
         dataRegion.readUnlock();
       }
-    } else {
+    } finally {
       addInitQueryDataSourceCost(System.nanoTime() - startTime);
-      return false;
     }
   }
 
@@ -927,16 +941,20 @@ public class FragmentInstanceContext extends QueryContext 
{
     if (pathList == null) {
       return true;
     }
-    long waitForLockTime = COMMON_CONFIG.getDriverTaskExecutionTimeSliceInMs();
-    if (dataRegion.tryReadLock(waitForLockTime)) {
-      // minus already consumed time
-      waitForLockTime -= (System.nanoTime() - startTime) / 1_000_000;
 
-      // no remaining time slice
-      if (waitForLockTime <= 0) {
+    try (DeferredFileReaderReleases deferredReleases = new 
DeferredFileReaderReleases()) {
+      long waitForLockTime = 
COMMON_CONFIG.getDriverTaskExecutionTimeSliceInMs();
+      if (!dataRegion.tryReadLock(waitForLockTime)) {
         return false;
       }
       try {
+        // minus already consumed time
+        waitForLockTime -= (System.nanoTime() - startTime) / 1_000_000;
+
+        // no remaining time slice
+        if (waitForLockTime <= 0) {
+          return false;
+        }
         this.sharedQueryDataSource =
             dataRegion.queryForSeriesRegionScan(
                 pathList,
@@ -948,19 +966,18 @@ public class FragmentInstanceContext extends QueryContext 
{
         if (sharedQueryDataSource != null) {
           closedFilePaths = new HashSet<>();
           unClosedFilePaths = new HashSet<>();
-          addUsedFilesForRegionQuery((QueryDataSourceForRegionScan) 
sharedQueryDataSource);
+          addUsedFilesForRegionQuery(
+              (QueryDataSourceForRegionScan) sharedQueryDataSource, 
deferredReleases);
           return true;
         } else {
           // failed to acquire lock within the specific time
           return false;
         }
       } finally {
-        addInitQueryDataSourceCost(System.nanoTime() - startTime);
         dataRegion.readUnlock();
       }
-    } else {
+    } finally {
       addInitQueryDataSourceCost(System.nanoTime() - startTime);
-      return false;
     }
   }
 
@@ -1021,24 +1038,78 @@ public class FragmentInstanceContext extends 
QueryContext {
     }
   }
 
+  /** Owns pending file references until they can be released outside region 
locks. */
+  static final class DeferredFileReaderReleases implements AutoCloseable {
+    private final List<Pair<TsFileResource, Boolean>> references = new 
ArrayList<>();
+
+    private void add(TsFileResource resource, boolean isClosed, 
Set<TsFileResource> owners) {
+      if (!owners.contains(resource)) {
+        return;
+      }
+      // Allocate the pending record before removing its old owner, including 
on allocation failure.
+      references.add(new Pair<>(resource, isClosed));
+      if (!owners.remove(resource)) {
+        references.remove(references.size() - 1);
+      }
+    }
+
+    void addAll(Set<TsFileResource> owners, boolean isClosed) {
+      Iterator<TsFileResource> iterator = owners.iterator();
+      while (iterator.hasNext()) {
+        references.add(new Pair<>(iterator.next(), isClosed));
+        iterator.remove();
+      }
+    }
+
+    @Override
+    public void close() {
+      Throwable failure = null;
+      try {
+        for (Pair<TsFileResource, Boolean> reference : references) {
+          try {
+            FileReaderManager.getInstance()
+                .decreaseFileReaderReference(reference.left, reference.right);
+          } catch (RuntimeException | Error e) {
+            if (failure == null) {
+              failure = e;
+            } else if (failure != e) {
+              failure.addSuppressed(e);
+            }
+          }
+        }
+      } finally {
+        references.clear();
+      }
+      if (failure instanceof Error) {
+        throw (Error) failure;
+      }
+      if (failure != null) {
+        throw (RuntimeException) failure;
+      }
+    }
+  }
+
   /** Lock and check if tsFileResource is deleted */
-  private boolean processTsFileResource(TsFileResource tsFileResource, boolean 
isClosed) {
-    return processTsFileResource(tsFileResource, isClosed, closedFilePaths, 
unClosedFilePaths);
+  private boolean processTsFileResource(
+      TsFileResource tsFileResource,
+      boolean isClosed,
+      DeferredFileReaderReleases deferredReleases) {
+    return processTsFileResource(
+        tsFileResource, isClosed, closedFilePaths, unClosedFilePaths, 
deferredReleases);
   }
 
   private boolean processTsFileResource(
       TsFileResource tsFileResource,
       boolean isClosed,
       Set<TsFileResource> closedResources,
-      Set<TsFileResource> unclosedResources) {
+      Set<TsFileResource> unclosedResources,
+      DeferredFileReaderReleases deferredReleases) {
     addFilePathToMap(tsFileResource, isClosed, closedResources, 
unclosedResources);
     // this file may be deleted just before we lock it
     if (tsFileResource.isDeleted()) {
       Set<TsFileResource> pathSet = isClosed ? closedResources : 
unclosedResources;
-      // This resource may be removed by other threads of this query.
-      if (pathSet.remove(tsFileResource)) {
-        
FileReaderManager.getInstance().decreaseFileReaderReference(tsFileResource, 
isClosed);
-      }
+      // Keep the file reference until region unlock; the last release may 
wait for reader I/O.
+      deferredReleases.add(tsFileResource, isClosed, pathSet);
       return true;
     } else {
       return false;
@@ -1049,7 +1120,8 @@ public class FragmentInstanceContext extends QueryContext 
{
   private void addUsedFilesForQuery(
       QueryDataSource dataSource,
       Set<TsFileResource> closedResources,
-      Set<TsFileResource> unclosedResources) {
+      Set<TsFileResource> unclosedResources,
+      DeferredFileReaderReleases deferredReleases) {
 
     // sequence data
     dataSource
@@ -1057,7 +1129,11 @@ public class FragmentInstanceContext extends 
QueryContext {
         .removeIf(
             tsFileResource ->
                 processTsFileResource(
-                    tsFileResource, tsFileResource.isClosed(), 
closedResources, unclosedResources));
+                    tsFileResource,
+                    tsFileResource.isClosed(),
+                    closedResources,
+                    unclosedResources,
+                    deferredReleases));
 
     // Record statistics of seqFiles
     int unclosedSeqFileCount = unclosedResources.size();
@@ -1071,19 +1147,25 @@ public class FragmentInstanceContext extends 
QueryContext {
         .removeIf(
             tsFileResource ->
                 processTsFileResource(
-                    tsFileResource, tsFileResource.isClosed(), 
closedResources, unclosedResources));
+                    tsFileResource,
+                    tsFileResource.isClosed(),
+                    closedResources,
+                    unclosedResources,
+                    deferredReleases));
 
     // Record statistics of files of unseqFiles
     unclosedUnseqFileNum.addAndGet(unclosedResources.size() - 
unclosedSeqFileCount);
     closedUnseqFileNum.addAndGet(closedResources.size() - closedSeqFileCount);
   }
 
-  private void addUsedFilesForRegionQuery(QueryDataSourceForRegionScan 
dataSource) {
+  private void addUsedFilesForRegionQuery(
+      QueryDataSourceForRegionScan dataSource, DeferredFileReaderReleases 
deferredReleases) {
     dataSource
         .getSeqFileScanHandles()
         .removeIf(
             fileScanHandle ->
-                processTsFileResource(fileScanHandle.getTsResource(), 
fileScanHandle.isClosed()));
+                processTsFileResource(
+                    fileScanHandle.getTsResource(), fileScanHandle.isClosed(), 
deferredReleases));
 
     int unclosedSeqFileCount = unClosedFilePaths.size();
     int closedSeqFileCount = closedFilePaths.size();
@@ -1094,7 +1176,8 @@ public class FragmentInstanceContext extends QueryContext 
{
         .getUnseqFileScanHandles()
         .removeIf(
             fileScanHandle ->
-                processTsFileResource(fileScanHandle.getTsResource(), 
fileScanHandle.isClosed()));
+                processTsFileResource(
+                    fileScanHandle.getTsResource(), fileScanHandle.isClosed(), 
deferredReleases));
 
     unclosedUnseqFileNum.addAndGet(unClosedFilePaths.size() - 
unclosedSeqFileCount);
     closedUnseqFileNum.addAndGet(closedFilePaths.size() - closedSeqFileCount);
@@ -1110,14 +1193,20 @@ public class FragmentInstanceContext extends 
QueryContext {
     addFilePathToMap(tsFile, isClosed, closedFilePaths, unClosedFilePaths);
   }
 
-  private void addFilePathToMap(
+  static void addFilePathToMap(
       TsFileResource tsFile,
       boolean isClosed,
       Set<TsFileResource> closedResources,
       Set<TsFileResource> unclosedResources) {
     Set<TsFileResource> pathSet = isClosed ? closedResources : 
unclosedResources;
     if (pathSet.add(tsFile)) {
-      FileReaderManager.getInstance().increaseFileReaderReference(tsFile, 
isClosed);
+      try {
+        FileReaderManager.getInstance().increaseFileReaderReference(tsFile, 
isClosed);
+      } catch (RuntimeException | Error e) {
+        // Registration already rolled back its read lock. Do not release it a 
second time later.
+        pathSet.remove(tsFile);
+        throw e;
+      }
     }
   }
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/control/FileReaderManager.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/control/FileReaderManager.java
index 429859f594f..a0b9d0f169f 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/control/FileReaderManager.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/control/FileReaderManager.java
@@ -31,347 +31,589 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.Iterator;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.Objects;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.LongConsumer;
 
 /**
- * {@link FileReaderManager} is a singleton, which is used to manage all file 
readers(opened file
- * streams) to ensure that each file is opened at most once.
+ * Caches readers for managed TsFiles and external file paths, with reader I/O 
serialized per file.
+ *
+ * <p>Each {@link Entry} is a stable lifecycle record and the monitor for its 
reader slots. An
+ * operation pin keeps the entry registered while a manager call uses it or 
waits for its monitor.
+ * Pins do not keep a returned reader open: callers retain a {@link 
Slot#references file-reader
+ * reference} for the duration of reader use, separately from each manager 
call's pin.
+ *
+ * <p>{@link #registryLock} protects registry membership, pins, and reference 
counts. Entry monitors
+ * protect reader construction, close, and the cached reader fields, not reads 
performed by callers
+ * after {@code get} returns. Never hold {@code registryLock} while acquiring 
an entry monitor or
+ * doing I/O. An entry may briefly acquire {@code registryLock} to recheck 
references, but must not
+ * acquire a resource lock, region lock, or another entry monitor. Reference 
registration uses only
+ * the short registry critical section, so callers can register while holding 
a region read lock.
  */
 public class FileReaderManager {
-
   private static final Logger logger = 
LoggerFactory.getLogger(FileReaderManager.class);
   private static final Logger resourceLogger = 
LoggerFactory.getLogger("FileMonitor");
-  private static final Logger DEBUG_LOGGER = 
LoggerFactory.getLogger("QUERY_DEBUG");
-
-  /** max number of file streams being cached, must be lower than 65535. */
   private static final int MAX_CACHED_FILE_SIZE = 30000;
+  private static final int PRINT_INTERVAL = 10000;
 
   /**
-   * When number of file streams reached MAX_CACHED_FILE_SIZE, then we will 
print a warning log each
-   * PRINT_INTERVAL.
+   * Manager-wide monitor for both registries, {@link #clearing}, both levels 
of pin counters, and
+   * {@link Slot#references}. It is never held across reader I/O or an 
entry-monitor acquisition.
    */
-  private static final int PRINT_INTERVAL = 10000;
+  private final Object registryLock = new Object();
+
+  /** Serializes entire global test-cleanup passes; ordinary reader operations 
do not acquire it. */
+  private final Object clearLock = new Object();
 
   /**
-   * the key of closedFileReaderMap is the file path and the value of 
closedFileReaderMap is the
-   * corresponding reader.
+   * Managed files keyed by TsFileID, each with independent closed/unclosed 
slots under one entry
+   * monitor. Guarded by {@link #registryLock}.
    */
-  private Map<TsFileID, TsFileSequenceReader> closedFileReaderMap;
+  private final Map<TsFileID, Entry> internal = new HashMap<>();
 
   /**
-   * the key of unclosedFileReaderMap is the file path and the value of 
unclosedFileReaderMap is the
-   * corresponding reader.
+   * External files keyed by the supplied path string, in a namespace separate 
from {@link
+   * #internal}. External entries use only their closed slot. Guarded by 
{@link #registryLock}.
    */
-  private Map<TsFileID, TsFileSequenceReader> unclosedFileReaderMap;
+  private final Map<String, Entry> external = new HashMap<>();
+
+  /** Cached closed-reader count for managed files; updated concurrently under 
different entries. */
+  private final AtomicInteger closedCount = new AtomicInteger();
+
+  /** Cached unclosed-reader count for managed files; independent of query 
references and pins. */
+  private final AtomicInteger unclosedCount = new AtomicInteger();
+
+  /** Cached-reader count for the external-path namespace. */
+  private final AtomicInteger externalCount = new AtomicInteger();
 
   /**
-   * the key of closedFileReaderMap is the file path and the value of 
closedFileReaderMap is the
-   * file's reference count.
+   * Test-cleanup admission gate: new acquisitions wait while releases remain 
allowed. Guarded by
+   * {@link #registryLock}; it remains set through the final pin drain and 
entry reclamation.
    */
-  private Map<TsFileID, AtomicInteger> closedReferenceMap;
+  private boolean clearing;
 
   /**
-   * the key of unclosedFileReaderMap is the file path and the value of 
unclosedFileReaderMap is the
-   * file's reference count.
+   * Sum of {@link Entry#pins} across both registries, including operations 
waiting for entry
+   * monitors. Global test cleanup drains this counter; zero does not imply 
zero query references.
+   * Guarded by {@link #registryLock}.
    */
-  private Map<TsFileID, AtomicInteger> unclosedReferenceMap;
-
-  /** External TsFile readers. The key is the file path. */
-  private Map<String, TsFileSequenceReader> externalFileReaderMap;
+  private int pins;
 
-  /** Reference count of external TsFile readers. */
-  private Map<String, AtomicInteger> externalReferenceMap;
-
-  private FileReaderManager() {
-    closedFileReaderMap = new ConcurrentHashMap<>();
-    unclosedFileReaderMap = new ConcurrentHashMap<>();
-    closedReferenceMap = new ConcurrentHashMap<>();
-    unclosedReferenceMap = new ConcurrentHashMap<>();
-    externalFileReaderMap = new ConcurrentHashMap<>();
-    externalReferenceMap = new ConcurrentHashMap<>();
-  }
+  private FileReaderManager() {}
 
   public static FileReaderManager getInstance() {
     return FileReaderManagerHelper.INSTANCE;
   }
 
-  public synchronized void closeFileAndRemoveReader(TsFileID tsFileID) throws 
IOException {
-    closedReferenceMap.remove(tsFileID);
-    TsFileSequenceReader reader = closedFileReaderMap.remove(tsFileID);
-    if (reader != null) {
-      reader.close();
+  /** One reader state (closed or unclosed) and its independently maintained 
query references. */
+  private static class Slot {
+    /**
+     * Nullable cached handle, created and discarded under the owning entry's 
monitor. Reclamation
+     * may inspect it under registryLock after that entry's operations have 
finished.
+     */
+    private TsFileSequenceReader reader;
+
+    /**
+     * Outstanding file-reader retains for this slot, guarded by registryLock. 
May be positive
+     * before any reader is opened; unlike operation pins, these retains span 
the caller's reader
+     * use.
+     */
+    private int references;
+
+    /**
+     * Shared cached-reader counter for this slot's namespace/state. 
Publishing a reader increments
+     * it; discarding a handle after a close attempt decrements it, even when 
close fails. This
+     * counts cached handles, not query references or all open OS descriptors.
+     */
+    private final AtomicInteger count;
+
+    private Slot(AtomicInteger count) {
+      this.count = count;
+    }
+
+    private void close() throws IOException {
+      if (reader != null) {
+        try {
+          reader.close();
+        } catch (IOException e) {
+          logger.error(
+              StorageEngineMessages.CANNOT_CLOSE_TSFILE_SEQUENCE_READER, 
reader.getFileName(), e);
+          throw e;
+        } finally {
+          // A failed close must neither poison future reads nor retain an 
unused entry forever.
+          // As with the original manager, discard the handle after a 
best-effort close. Pins keep
+          // this entry stable until the attempt finishes, including when 
close throws unchecked.
+          reader = null;
+          count.decrementAndGet();
+        }
+      }
+    }
+
+    private boolean empty() {
+      return reader == null && references == 0;
+    }
+  }
+
+  /**
+   * Stable per-key record whose monitor serializes reader lifecycle changes. 
It cannot be reclaimed
+   * while an operation is pinned, even if both slots are empty, because a 
waiter may still use this
+   * exact monitor. Replacing it early would allow two entries to open readers 
for the same key.
+   */
+  private static class Entry {
+    /** Closed-file reader slot; also the sole slot used by external entries. 
*/
+    private final Slot closed;
+
+    /** Unclosed-file reader slot for managed files; unused for external 
entries. */
+    private final Slot unclosed;
+
+    /**
+     * Manager operations retaining this entry, including monitor waiters; 
guarded by registryLock.
+     */
+    private int pins;
+
+    /**
+     * @param closedCount managed closed-reader counter or external-reader 
counter, depending on the
+     *     registry that owns this entry
+     * @param unclosedCount managed unclosed-reader counter; external entries 
never use this slot
+     */
+    private Entry(AtomicInteger closedCount, AtomicInteger unclosedCount) {
+      closed = new Slot(closedCount);
+      unclosed = new Slot(unclosedCount);
+    }
+
+    private Slot slot(boolean isClosed) {
+      return isClosed ? closed : unclosed;
     }
-    unclosedReferenceMap.remove(tsFileID);
-    reader = unclosedFileReaderMap.remove(tsFileID);
-    if (reader != null) {
-      reader.close();
+
+    private boolean empty() {
+      return closed.empty() && unclosed.empty();
     }
   }
 
   /**
-   * Get the reader of the file(tsfile or unseq tsfile) indicated by filePath. 
If the reader already
-   * exists, just get it from closedFileReaderMap or unclosedFileReaderMap 
depending on isClosing .
-   * Otherwise a new reader will be created and cached.
+   * Looks up an entry and retains its identity for one manager operation, 
optionally creating it.
+   *
+   * <p>The operation is counted before the caller can wait for the entry 
monitor, so reclamation
+   * cannot replace an entry that a waiter still holds. This method neither 
acquires that monitor,
+   * opens a reader, nor registers a query reference. Every non-null result 
must be paired with
+   * exactly one {@code unpin} in a {@code finally} block, after leaving the 
entry monitor.
    *
-   * @param filePath the path of the tsfile
-   * @param tsFileID the id of the tsfile, of which the reader is desired.
-   * @param isClosed whether the corresponding file still receives insertions 
or not.
-   * @return the reader of the file specified by filePath.
-   * @throws IOException when reader cannot be created.
+   * <p>Admission waits are uninterruptible: an interrupt is remembered and 
restored before this
+   * method returns. Releases bypass the gate so they can complete during 
global test cleanup.
+   *
+   * @param <K> TsFileID for the internal registry, or String for the external 
registry
+   * @param registry exactly {@link #internal} or {@link #external}; also 
selects the cached-reader
+   *     counter for a newly created entry
+   * @param key non-null file identity within the selected registry
+   * @param admission true to wait until global test cleanup reopens 
admission; false for reference
+   *     releases that must remain allowed during cleanup
+   * @param create true to install an empty entry if the key is absent; false 
to return null instead
+   * @return the entry with both its own and the global operation-pin count 
incremented, or null if
+   *     the key is absent and creation is disabled
    */
-  @SuppressWarnings("squid:S2095")
-  public synchronized TsFileSequenceReader get(String filePath, TsFileID 
tsFileID, boolean isClosed)
-      throws IOException {
-    return get(filePath, tsFileID, isClosed, null);
+  private <K> Entry pin(Map<K, Entry> registry, K key, boolean admission, 
boolean create) {
+    Objects.requireNonNull(key);
+    boolean interrupted = false;
+    synchronized (registryLock) {
+      while (admission && clearing) {
+        try {
+          registryLock.wait();
+        } catch (InterruptedException e) {
+          interrupted = true;
+        }
+      }
+      if (interrupted) {
+        Thread.currentThread().interrupt();
+      }
+      Entry entry = registry.get(key);
+      if (entry == null && create) {
+        entry = new Entry(registry == internal ? closedCount : externalCount, 
unclosedCount);
+        registry.put(key, entry);
+      }
+      if (entry != null) {
+        entry.pins++;
+        pins++;
+      }
+      return entry;
+    }
   }
 
   /**
-   * Get the reader of the file(tsfile or unseq tsfile) indicated by filePath. 
If the reader already
-   * exists, just get it from closedFileReaderMap or unclosedFileReaderMap 
depending on isClosing .
-   * Otherwise a new reader will be created and cached.
+   * Releases the matching operation pin without changing query references or 
closing a reader.
+   * Outside global cleanup, an empty entry is reclaimed only when no 
operation still retains it.
+   * Call after leaving the entry monitor, including when the operation fails.
    *
-   * @param filePath the path of the tsfile
-   * @param tsFileID the id of the tsfile, of which the reader is desired.
-   * @param isClosed whether the corresponding file still receives insertions 
or not.
-   * @param ioSizeRecorder can be null
-   * @return the reader of the file specified by filePath.
-   * @throws IOException when reader cannot be created.
+   * @param <K> key type of the selected registry
+   * @param registry the same registry supplied to the matching {@code pin}
+   * @param key the same file identity supplied to the matching {@code pin}
+   * @param entry the exact non-null entry returned by that call, not a fresh 
registry lookup
    */
-  @SuppressWarnings("squid:S2095")
-  public synchronized TsFileSequenceReader get(
+  private <K> void unpin(Map<K, Entry> registry, K key, Entry entry) {
+    synchronized (registryLock) {
+      entry.pins--;
+      pins--;
+      // At zero pins all slot mutations have completed and published through 
registryLock.
+      // Keep entries stable during clear, including those used by releases 
entering mid-clear.
+      if (!clearing && entry.pins == 0 && entry.empty()) {
+        registry.remove(key, entry);
+      }
+      if (clearing && pins == 0) {
+        registryLock.notifyAll();
+      }
+    }
+  }
+
+  /** Caller must exclude new references and finish existing reader use 
(resource write lock). */
+  public void closeFileAndRemoveReader(TsFileID tsFileID) throws IOException {
+    Entry entry = pin(internal, tsFileID, true, false);
+    if (entry == null) {
+      return;
+    }
+    try {
+      synchronized (entry) {
+        closeEntry(entry);
+      }
+    } finally {
+      unpin(internal, tsFileID, entry);
+    }
+  }
+
+  private void closeEntry(Entry entry) throws IOException {
+    synchronized (registryLock) {
+      entry.closed.references = 0;
+      entry.unclosed.references = 0;
+    }
+    IOException failure = null;
+    try {
+      try {
+        closeSlot(entry.closed, 
StorageEngineMessages.CLOSED_TSFILE_READER_CLOSED);
+      } catch (IOException e) {
+        failure = mergeFailure(failure, e);
+      }
+    } finally {
+      // Also attempt the second slot if the first close throws an unchecked 
exception.
+      try {
+        closeSlot(entry.unclosed, 
StorageEngineMessages.UNCLOSED_TSFILE_READER_CLOSED);
+      } catch (IOException e) {
+        failure = mergeFailure(failure, e);
+      }
+    }
+    if (failure != null) {
+      throw failure;
+    }
+  }
+
+  private static void closeSlot(Slot slot, String message) throws IOException {
+    TsFileSequenceReader reader = slot.reader;
+    slot.close();
+    if (reader != null && resourceLogger.isDebugEnabled()) {
+      resourceLogger.debug(message, reader.getFileName());
+    }
+  }
+
+  private static IOException mergeFailure(IOException failure, IOException 
next) {
+    if (failure == null) {
+      return new IOException(next);
+    }
+    // Never attach other files' errors to an exception supplied by a reader.
+    failure.addSuppressed(next);
+    return failure;
+  }
+
+  public TsFileSequenceReader get(String filePath, TsFileID tsFileID, boolean 
isClosed)
+      throws IOException {
+    return get(filePath, tsFileID, isClosed, null);
+  }
+
+  public TsFileSequenceReader get(
       String filePath, TsFileID tsFileID, boolean isClosed, LongConsumer 
ioSizeRecorder)
       throws IOException {
     return get(filePath, tsFileID, isClosed, ioSizeRecorder, false);
   }
 
-  @SuppressWarnings("squid:S2095")
-  public synchronized TsFileSequenceReader get(
+  /** Get does not acquire a query reference. A failed constructor leaves 
registered refs intact. */
+  public TsFileSequenceReader get(
       String filePath,
       TsFileID tsFileID,
       boolean isClosed,
       LongConsumer ioSizeRecorder,
       boolean isExternalTsFile)
       throws IOException {
-    if (isExternalTsFile) {
-      return getExternalTsFileReader(filePath, ioSizeRecorder);
-    }
-
-    Map<TsFileID, TsFileSequenceReader> readerMap =
-        !isClosed ? unclosedFileReaderMap : closedFileReaderMap;
-    if (!readerMap.containsKey(tsFileID)) {
-      int currentOpenedReaderCount = readerMap.size();
-      if (currentOpenedReaderCount >= MAX_CACHED_FILE_SIZE
-          && (currentOpenedReaderCount % PRINT_INTERVAL == 0)) {
-        logger.warn(StorageEngineMessages.QUERY_OPENED_FILES, 
readerMap.size());
-      }
-
-      TsFileSequenceReader tsFileReader = null;
-      // check if the file is old version
-      if (!isClosed) {
-        tsFileReader =
-            new UnClosedTsFileReader(
-                filePath,
-                EncryptDBUtils.getFirstEncryptParamFromTSFilePath(filePath),
-                ioSizeRecorder);
-      } else {
-        // already do the version check in TsFileSequenceReader's constructor
-        tsFileReader =
-            new TsFileSequenceReader(
-                filePath,
-                ioSizeRecorder,
-                EncryptDBUtils.getFirstEncryptParamFromTSFilePath(filePath));
-      }
-      readerMap.put(tsFileID, tsFileReader);
-      return tsFileReader;
-    }
-
-    return readerMap.get(tsFileID);
+    return isExternalTsFile
+        ? get(external, filePath, filePath, true, ioSizeRecorder)
+        : get(internal, tsFileID, filePath, isClosed, ioSizeRecorder);
   }
 
-  private TsFileSequenceReader getExternalTsFileReader(String filePath, 
LongConsumer ioSizeRecorder)
+  @SuppressWarnings("squid:S2095")
+  private <K> TsFileSequenceReader get(
+      Map<K, Entry> registry, K key, String path, boolean isClosed, 
LongConsumer recorder)
       throws IOException {
-    TsFileSequenceReader reader = externalFileReaderMap.get(filePath);
-    if (reader == null) {
-      int currentOpenedReaderCount = externalFileReaderMap.size();
-      if (currentOpenedReaderCount >= MAX_CACHED_FILE_SIZE
-          && (currentOpenedReaderCount % PRINT_INTERVAL == 0)) {
-        logger.warn(StorageEngineMessages.QUERY_OPENED_FILES, 
externalFileReaderMap.size());
+    Entry entry = pin(registry, key, true, true);
+    try {
+      synchronized (entry) {
+        Slot slot = entry.slot(isClosed);
+        if (slot.reader == null) {
+          slot.reader =
+              isClosed
+                  ? new TsFileSequenceReader(
+                      path, recorder, 
EncryptDBUtils.getFirstEncryptParamFromTSFilePath(path))
+                  : new UnClosedTsFileReader(
+                      path, 
EncryptDBUtils.getFirstEncryptParamFromTSFilePath(path), recorder);
+          int count = slot.count.incrementAndGet();
+          if (count >= MAX_CACHED_FILE_SIZE && count % PRINT_INTERVAL == 0) {
+            logger.warn(StorageEngineMessages.QUERY_OPENED_FILES, count);
+          }
+        }
+        return slot.reader;
       }
-      reader =
-          new TsFileSequenceReader(
-              filePath,
-              ioSizeRecorder,
-              EncryptDBUtils.getFirstEncryptParamFromTSFilePath(filePath));
-      externalFileReaderMap.put(filePath, reader);
+    } finally {
+      unpin(registry, key, entry);
     }
-    return reader;
   }
 
   /**
-   * Increase the reference count of the reader specified by filePath. Only 
when the reference count
-   * of a reader equals zero, the reader can be closed and removed.
+   * Retains one reader reference and the resource read lock until the 
matching release.
+   *
+   * @param tsFile resource whose reader will be used
+   * @param isClosed slot to retain; pass this same value when releasing, even 
if the file is sealed
+   *     in the meantime
    */
   public void increaseFileReaderReference(TsFileResource tsFile, boolean 
isClosed) {
     tsFile.readLock();
-    synchronized (this) {
-      if (!isClosed) {
-        unclosedReferenceMap
-            .computeIfAbsent(tsFile.getTsFileID(), k -> new AtomicInteger())
-            .getAndIncrement();
-      } else {
-        closedReferenceMap
-            .computeIfAbsent(tsFile.getTsFileID(), k -> new AtomicInteger())
-            .getAndIncrement();
+    boolean registered = false;
+    try {
+      increase(internal, tsFile.getTsFileID(), isClosed);
+      registered = true;
+    } finally {
+      if (!registered) {
+        tsFile.readUnlock();
       }
     }
   }
 
-  public synchronized void increaseExternalFileReaderReference(String 
filePath) {
-    externalReferenceMap.computeIfAbsent(filePath, k -> new 
AtomicInteger()).getAndIncrement();
+  public void increaseExternalFileReaderReference(String filePath) {
+    increase(external, filePath, true);
+  }
+
+  private <K> void increase(Map<K, Entry> registry, K key, boolean isClosed) {
+    // One short registry critical section, with no wait for an opening or 
closing reader.
+    synchronized (registryLock) {
+      Entry entry = pin(registry, key, true, true);
+      try {
+        entry.slot(isClosed).references++;
+      } finally {
+        unpin(registry, key, entry);
+      }
+    }
   }
 
   /**
-   * Decrease the reference count of the reader specified by filePath. This 
method is latch-free.
-   * Only when the reference count of a reader equals zero, the reader can be 
closed and removed.
+   * Releases one retained reference and its resource read lock. The resource 
lock is released even
+   * if global test cleanup has already removed the reader reference.
+   *
+   * @param tsFile resource from the matching successful registration; release 
it exactly once
+   * @param isClosed slot selected at registration, not the file's current 
sealed state
    */
   public void decreaseFileReaderReference(TsFileResource tsFile, boolean 
isClosed) {
-    synchronized (this) {
-      if (!isClosed && unclosedReferenceMap.containsKey(tsFile.getTsFileID())) 
{
-        if (unclosedReferenceMap.get(tsFile.getTsFileID()).decrementAndGet() 
== 0) {
-          closeUnUsedReaderAndRemoveRef(tsFile.getTsFilePath(), 
tsFile.getTsFileID(), false);
-        }
-      } else if (closedReferenceMap.containsKey(tsFile.getTsFileID())
-          && (closedReferenceMap.get(tsFile.getTsFileID()).decrementAndGet() 
== 0)) {
-        closeUnUsedReaderAndRemoveRef(tsFile.getTsFilePath(), 
tsFile.getTsFileID(), true);
-      }
+    try {
+      decrease(internal, tsFile.getTsFileID(), isClosed);
+    } finally {
+      tsFile.readUnlock();
     }
-    tsFile.readUnlock();
   }
 
-  public synchronized void decreaseExternalFileReaderReference(String 
filePath) {
-    AtomicInteger reference = externalReferenceMap.get(filePath);
-    if (reference != null && reference.decrementAndGet() == 0) {
-      closeUnUsedExternalReaderAndRemoveRef(filePath);
-    }
+  public void decreaseExternalFileReaderReference(String filePath) {
+    decrease(external, filePath, true);
   }
 
-  private void closeUnUsedExternalReaderAndRemoveRef(String readerKey) {
-    synchronized (this) {
-      AtomicInteger reference = externalReferenceMap.get(readerKey);
-      if (reference != null && reference.get() != 0) {
-        return;
+  private <K> void decrease(Map<K, Entry> registry, K key, boolean isClosed) {
+    Entry entry = pin(registry, key, false, false);
+    if (entry == null) {
+      return;
+    }
+    try {
+      Slot slot;
+      synchronized (registryLock) {
+        // Sealing a file does not migrate existing references between slots.
+        slot = entry.slot(isClosed);
+        if (slot.references == 0 || --slot.references != 0) {
+          return;
+        }
       }
-
-      TsFileSequenceReader reader = externalFileReaderMap.get(readerKey);
-      if (reader != null) {
+      synchronized (entry) {
+        synchronized (registryLock) {
+          // A new query may have registered while this release waited for 
reader I/O.
+          if (slot.references != 0) {
+            return;
+          }
+        }
+        // New references may now register, but their get() waits for this 
entry's close to finish.
         try {
-          reader.close();
+          closeSlot(
+              slot,
+              slot == entry.closed
+                  ? StorageEngineMessages
+                      
.LOG_READER_FOR_CLOSED_TSFILE_ARG_IS_CLOSED_BECAUSE_ITS_REFERENCE_COUNT_REACHED_ZERO_C3B71A85
+                  : StorageEngineMessages
+                      
.LOG_READER_FOR_UNCLOSED_TSFILE_ARG_IS_CLOSED_BECAUSE_ITS_REFERENCE_COUNT_REACHED_ZERO_088BDEF8);
         } catch (IOException e) {
-          logger.error(
-              StorageEngineMessages.CANNOT_CLOSE_TSFILE_SEQUENCE_READER, 
reader.getFileName(), e);
+          // Slot.close already logged the error and discarded the unusable 
handle.
         }
       }
-      externalFileReaderMap.remove(readerKey);
-      externalReferenceMap.remove(readerKey);
-      if (resourceLogger.isDebugEnabled()) {
-        resourceLogger.debug(
-            "{} externalTsFileReader is closed because of no reference.", 
readerKey);
-      }
+    } finally {
+      unpin(registry, key, entry);
     }
   }
 
-  private void closeUnUsedReaderAndRemoveRef(
-      String tsFilePath, TsFileID tsFileID, boolean isClosed) {
-    Map<TsFileID, TsFileSequenceReader> readerMap =
-        isClosed ? closedFileReaderMap : unclosedFileReaderMap;
-    Map<TsFileID, AtomicInteger> refMap = isClosed ? closedReferenceMap : 
unclosedReferenceMap;
-    synchronized (this) {
-      // check ref num again
-      if (refMap.get(tsFileID).get() != 0) {
-        return;
-      }
-
-      TsFileSequenceReader reader = readerMap.get(tsFileID);
-      if (reader != null) {
+  /**
+   * Test cleanup only: callers must first stop reader use. Zero operation 
pins does not mean zero
+   * query references. Acquisitions wait; releases remain admitted throughout 
cleanup. Interrupts
+   * are restored only after cleanup and the final release-pin drain, never 
reopening the gate
+   * early.
+   */
+  @TestOnly
+  public void closeAndRemoveAllOpenedReaders() throws IOException {
+    synchronized (clearLock) {
+      boolean interrupted = Thread.interrupted();
+      IOException failure = null;
+      try {
+        List<Entry> entries;
+        synchronized (registryLock) {
+          clearing = true;
+          interrupted |= drainPins();
+          entries = new ArrayList<>(internal.values());
+          entries.addAll(external.values());
+        }
+        for (Entry entry : entries) {
+          synchronized (entry) {
+            try {
+              closeEntry(entry);
+            } catch (IOException e) {
+              failure = mergeFailure(failure, e);
+            }
+          }
+          interrupted |= Thread.interrupted();
+        }
+      } finally {
         try {
-          reader.close();
-        } catch (IOException e) {
-          logger.error(
-              StorageEngineMessages.CANNOT_CLOSE_TSFILE_SEQUENCE_READER, 
reader.getFileName(), e);
+          synchronized (registryLock) {
+            try {
+              // Drain and reclaim before reopening admission: releases may 
still retain an entry.
+              interrupted |= Thread.interrupted();
+              interrupted |= drainPins();
+              internal.values().removeIf(Entry::empty);
+              external.values().removeIf(Entry::empty);
+            } finally {
+              // Even a failed snapshot or reclamation must not permanently 
close the test gate.
+              clearing = false;
+              registryLock.notifyAll();
+            }
+          }
+        } finally {
+          if (interrupted) {
+            Thread.currentThread().interrupt();
+          }
         }
       }
-      readerMap.remove(tsFileID);
-      refMap.remove(tsFileID);
-      if (resourceLogger.isDebugEnabled()) {
-        resourceLogger.debug(
-            StorageEngineMessages.TSFILE_READER_CLOSED_BECAUSE_NO_REFERENCE, 
tsFilePath);
+      if (failure != null) {
+        throw failure;
       }
     }
   }
 
   /**
-   * Only for <code>EnvironmentUtils.cleanEnv</code> method. To make sure that 
unit tests and
-   * integration tests will not conflict with each other.
+   * Waits for all currently admitted manager operations to finish during 
global test cleanup.
+   * Requires registryLock; waiting releases that monitor so operations can 
finish and unpin. This
+   * drains operation pins, not query references, and does not acquire any 
entry monitor.
    *
-   * @throws IOException if failed to close file handlers, IOException will be 
thrown
+   * @return whether an interrupt was consumed while waiting; the cleanup 
caller restores it after
+   *     completing cleanup and reopening admission
    */
-  public synchronized void closeAndRemoveAllOpenedReaders() throws IOException 
{
-    Iterator<Map.Entry<TsFileID, TsFileSequenceReader>> iterator =
-        closedFileReaderMap.entrySet().iterator();
-    while (iterator.hasNext()) {
-      Map.Entry<TsFileID, TsFileSequenceReader> entry = iterator.next();
-      entry.getValue().close();
-      if (resourceLogger.isDebugEnabled()) {
-        
resourceLogger.debug(StorageEngineMessages.CLOSED_TSFILE_READER_CLOSED, 
entry.getKey());
+  private boolean drainPins() {
+    boolean interrupted = false;
+    while (pins != 0) {
+      try {
+        registryLock.wait();
+      } catch (InterruptedException e) {
+        interrupted = true;
       }
-      closedReferenceMap.remove(entry.getKey());
-      iterator.remove();
     }
-    iterator = unclosedFileReaderMap.entrySet().iterator();
-    while (iterator.hasNext()) {
-      Map.Entry<TsFileID, TsFileSequenceReader> entry = iterator.next();
-      entry.getValue().close();
-      if (resourceLogger.isDebugEnabled()) {
-        
resourceLogger.debug(StorageEngineMessages.UNCLOSED_TSFILE_READER_CLOSED, 
entry.getKey());
-      }
-      unclosedReferenceMap.remove(entry.getKey());
-      iterator.remove();
+    return interrupted;
+  }
+
+  @TestOnly
+  public boolean contains(TsFileResource tsFile, boolean isClosed) {
+    TsFileID key = tsFile.getTsFileID();
+    Entry entry = pin(internal, key, true, false);
+    if (entry == null) {
+      return false;
     }
-    Iterator<Map.Entry<String, TsFileSequenceReader>> externalIterator =
-        externalFileReaderMap.entrySet().iterator();
-    while (externalIterator.hasNext()) {
-      Map.Entry<String, TsFileSequenceReader> entry = externalIterator.next();
-      entry.getValue().close();
-      if (resourceLogger.isDebugEnabled()) {
-        resourceLogger.debug("{} externalTsFileReader is closed.", 
entry.getKey());
+    try {
+      synchronized (entry) {
+        return entry.slot(isClosed).reader != null;
       }
-      externalReferenceMap.remove(entry.getKey());
-      externalIterator.remove();
+    } finally {
+      unpin(internal, key, entry);
     }
   }
 
-  /** This method is only for unit tests. */
-  public synchronized boolean contains(TsFileResource tsFile, boolean 
isClosed) {
-    return (isClosed && closedFileReaderMap.containsKey(tsFile.getTsFileID()))
-        || (!isClosed && 
unclosedFileReaderMap.containsKey(tsFile.getTsFileID()));
-  }
-
+  /** Snapshot for tests; modifying it does not mutate the manager. */
   @TestOnly
   public Map<TsFileID, TsFileSequenceReader> getClosedFileReaderMap() {
-    return closedFileReaderMap;
+    return snapshot(true);
   }
 
   @TestOnly
   public Map<TsFileID, TsFileSequenceReader> getUnclosedFileReaderMap() {
-    return unclosedFileReaderMap;
+    return snapshot(false);
   }
 
-  private static class FileReaderManagerHelper {
+  private Map<TsFileID, TsFileSequenceReader> snapshot(boolean isClosed) {
+    List<TsFileID> keys;
+    synchronized (registryLock) {
+      keys = new ArrayList<>(internal.keySet());
+    }
+    Map<TsFileID, TsFileSequenceReader> result = new HashMap<>();
+    for (TsFileID key : keys) {
+      Entry entry = pin(internal, key, true, false);
+      if (entry != null) {
+        try {
+          synchronized (entry) {
+            if (entry.slot(isClosed).reader != null) {
+              result.put(key, entry.slot(isClosed).reader);
+            }
+          }
+        } finally {
+          unpin(internal, key, entry);
+        }
+      }
+    }
+    return result;
+  }
 
+  @TestOnly
+  public void setReaderForTest(TsFileID key, boolean isClosed, 
TsFileSequenceReader reader)
+      throws IOException {
+    Entry entry = pin(internal, key, true, true);
+    try {
+      synchronized (entry) {
+        Slot slot = entry.slot(isClosed);
+        slot.close();
+        slot.reader = reader;
+        if (reader != null) {
+          slot.count.incrementAndGet();
+        }
+      }
+    } finally {
+      unpin(internal, key, entry);
+    }
+  }
+
+  private static class FileReaderManagerHelper {
     private static final FileReaderManager INSTANCE = new FileReaderManager();
 
     private FileReaderManagerHelper() {}
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/tsfile/TsFileLock.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/tsfile/TsFileLock.java
index ff5fe58919f..3fddd51f4b7 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/tsfile/TsFileLock.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/tsfile/TsFileLock.java
@@ -39,18 +39,20 @@ public class TsFileLock {
   private volatile int writeCnt;
 
   public void readLock() {
+    boolean interrupted = false;
     synchronized (this) {
       while (writeCnt > 0) {
         try {
-          this.wait(1);
+          this.wait();
         } catch (InterruptedException e) {
-          Thread.currentThread().interrupt();
-          // ignore
+          interrupted = true;
         }
       }
-
       readCnt++;
     }
+    if (interrupted) {
+      Thread.currentThread().interrupt();
+    }
   }
 
   public void readUnlock() {
@@ -63,18 +65,21 @@ public class TsFileLock {
   }
 
   public void writeLock() {
+    boolean interrupted = false;
     synchronized (this) {
-      try {
-        while (writeCnt > 0 || readCnt > 0) {
-          this.wait(1);
+      while (writeCnt > 0 || readCnt > 0) {
+        try {
+          this.wait();
+        } catch (InterruptedException e) {
+          // This API is uninterruptible: preserve exclusion, then restore the 
interrupt status.
+          interrupted = true;
         }
-      } catch (InterruptedException e) {
-        Thread.currentThread().interrupt();
-        // ignore
       }
-
       writeCnt++;
     }
+    if (interrupted) {
+      Thread.currentThread().interrupt();
+    }
   }
 
   public void writeUnlock() {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/FileReaderReferenceLifecycleTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/FileReaderReferenceLifecycleTest.java
new file mode 100644
index 00000000000..97ba208663a
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/FileReaderReferenceLifecycleTest.java
@@ -0,0 +1,189 @@
+/*
+ * 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.path.IFullPath;
+import org.apache.iotdb.db.storageengine.dataregion.DataRegion;
+import org.apache.iotdb.db.storageengine.dataregion.read.QueryDataSource;
+import 
org.apache.iotdb.db.storageengine.dataregion.read.control.FileReaderManager;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+import 
org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResourceStatus;
+
+import org.apache.tsfile.read.TsFileSequenceReader;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class FileReaderReferenceLifecycleTest {
+  @Rule public TemporaryFolder folder = new TemporaryFolder();
+  private final FileReaderManager manager = FileReaderManager.getInstance();
+
+  @After
+  public void tearDown() throws Exception {
+    manager.closeAndRemoveAllOpenedReaders();
+  }
+
+  private TsFileResource resource(int id) {
+    return new TsFileResource(new File(folder.getRoot(), id + 
"-1-0-0.tsfile"));
+  }
+
+  @Test
+  public void failedRegistrationIsRemovedFromFragmentReleaseSet() throws 
Exception {
+    TsFileResource resource = spy(resource(1));
+    Set<TsFileResource> closed = new HashSet<>();
+    Set<TsFileResource> unclosed = new HashSet<>();
+    // An unrelated query already owns a read lock. A failed registration must 
not repay that lock.
+    resource.readLock();
+    try {
+      doThrow(new OutOfMemoryError("injected registration failure"))
+          .doCallRealMethod()
+          .when(resource)
+          .getTsFileID();
+      assertThrows(
+          OutOfMemoryError.class,
+          () -> FragmentInstanceContext.addFilePathToMap(resource, true, 
closed, unclosed));
+      assertTrue(closed.isEmpty());
+      closed.forEach(file -> manager.decreaseFileReaderReference(file, true));
+      assertFalse(resource.tryWriteLock());
+    } finally {
+      resource.readUnlock();
+    }
+    assertTrue(resource.tryWriteLock());
+    resource.writeUnlock();
+  }
+
+  private FakedFragmentInstanceContext context(TsFileResource... resources) 
throws Exception {
+    DataRegion region = mock(DataRegion.class);
+    when(region.tryReadLock(anyLong())).thenReturn(true);
+    when(region.query(anyList(), any(), any(), any(), any(), anyLong()))
+        .thenReturn(new QueryDataSource(Arrays.asList(resources), 
Collections.emptyList()));
+    return new FakedFragmentInstanceContext(null, region);
+  }
+
+  @Test
+  public void fakedContextReleasesTheRegistrationTimeSlotAfterSealing() throws 
Exception {
+    TsFileResource resource = resource(1);
+    resource.setStatus(TsFileResourceStatus.UNCLOSED);
+    FakedFragmentInstanceContext context = context(resource);
+    context.initQueryDataSource(mock(IFullPath.class));
+    TsFileSequenceReader unclosed = mock(TsFileSequenceReader.class);
+    manager.setReaderForTest(resource.getTsFileID(), false, unclosed);
+    resource.setStatus(TsFileResourceStatus.NORMAL);
+    manager.increaseFileReaderReference(resource, true);
+    TsFileSequenceReader closed = mock(TsFileSequenceReader.class);
+    manager.setReaderForTest(resource.getTsFileID(), true, closed);
+    try {
+      context.releaseSharedQueryDataSource();
+      context.releaseSharedQueryDataSource();
+      verify(unclosed).close();
+      verify(closed, never()).close();
+      assertFalse(manager.contains(resource, false));
+      assertTrue(manager.contains(resource, true));
+      assertFalse(resource.tryWriteLock());
+    } finally {
+      manager.decreaseFileReaderReference(resource, true);
+    }
+    verify(closed).close();
+    assertTrue(resource.tryWriteLock());
+    resource.writeUnlock();
+  }
+
+  @Test
+  public void fakedContextRollsBackOnlySuccessfulRegistrations() throws 
Exception {
+    TsFileResource first = resource(1);
+    TsFileResource failing = spy(resource(2));
+    FakedFragmentInstanceContext context = context(first, failing);
+    failing.readLock();
+    try {
+      doThrow(new OutOfMemoryError("injected registration failure"))
+          .doCallRealMethod()
+          .when(failing)
+          .getTsFileID();
+      assertThrows(
+          OutOfMemoryError.class, () -> 
context.initQueryDataSource(mock(IFullPath.class)));
+      context.releaseSharedQueryDataSource();
+      assertTrue(first.tryWriteLock());
+      first.writeUnlock();
+      assertFalse(failing.tryWriteLock());
+    } finally {
+      failing.readUnlock();
+    }
+    assertTrue(failing.tryWriteLock());
+    failing.writeUnlock();
+  }
+
+  @Test
+  public void fakedRollbackPreservesOriginalFailureAndReleasesEveryReference() 
throws Exception {
+    TsFileResource first = resource(1);
+    TsFileResource second = resource(2);
+    TsFileResource failing = spy(resource(3));
+    first.setStatus(TsFileResourceStatus.NORMAL);
+    second.setStatus(TsFileResourceStatus.NORMAL);
+    FakedFragmentInstanceContext context = context(first, second, failing);
+    TsFileSequenceReader firstReader = mock(TsFileSequenceReader.class);
+    TsFileSequenceReader secondReader = mock(TsFileSequenceReader.class);
+    manager.setReaderForTest(first.getTsFileID(), true, firstReader);
+    manager.setReaderForTest(second.getTsFileID(), true, secondReader);
+    RuntimeException registrationFailure =
+        new IllegalStateException("injected registration failure");
+    RuntimeException closeFailure = new IllegalStateException("injected 
unchecked close failure");
+    doThrow(registrationFailure).when(failing).getTsFileID();
+    doThrow(closeFailure).when(firstReader).close();
+    first.readLock();
+    try {
+      RuntimeException actual =
+          assertThrows(
+              RuntimeException.class, () -> 
context.initQueryDataSource(mock(IFullPath.class)));
+      assertSame(registrationFailure, actual);
+      assertEquals(1, actual.getSuppressed().length);
+      assertSame(closeFailure, actual.getSuppressed()[0]);
+      verify(firstReader).close();
+      verify(secondReader).close();
+      assertTrue(second.tryWriteLock());
+      second.writeUnlock();
+      context.releaseSharedQueryDataSource();
+      assertFalse(first.tryWriteLock());
+      assertTrue(manager.getClosedFileReaderMap().isEmpty());
+    } finally {
+      first.readUnlock();
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/RegionQueryInitializationLockTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/RegionQueryInitializationLockTest.java
new file mode 100644
index 00000000000..9fbac4790cc
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/RegionQueryInitializationLockTest.java
@@ -0,0 +1,280 @@
+/*
+ * 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.conf.CommonDescriptor;
+import org.apache.iotdb.commons.path.IFullPath;
+import org.apache.iotdb.db.storageengine.dataregion.DataRegion;
+import org.apache.iotdb.db.storageengine.dataregion.read.QueryDataSource;
+import 
org.apache.iotdb.db.storageengine.dataregion.read.QueryDataSourceForRegionScan;
+import 
org.apache.iotdb.db.storageengine.dataregion.read.control.FileReaderManager;
+import 
org.apache.iotdb.db.storageengine.dataregion.read.filescan.IFileScanHandle;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+import 
org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResourceStatus;
+
+import org.apache.tsfile.read.TsFileSequenceReader;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class RegionQueryInitializationLockTest {
+  @Rule public TemporaryFolder folder = new TemporaryFolder();
+  private final FileReaderManager manager = FileReaderManager.getInstance();
+  private ExecutorService workers;
+  private int originalTimeSlice;
+
+  @Before
+  public void setUp() throws Exception {
+    manager.closeAndRemoveAllOpenedReaders();
+    originalTimeSlice =
+        
CommonDescriptor.getInstance().getConfig().getDriverTaskExecutionTimeSliceInMs();
+    
CommonDescriptor.getInstance().getConfig().setDriverTaskExecutionTimeSliceInMs(10_000);
+    workers = Executors.newFixedThreadPool(3);
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    workers.shutdownNow();
+    assertTrue(workers.awaitTermination(10, TimeUnit.SECONDS));
+    manager.closeAndRemoveAllOpenedReaders();
+    CommonDescriptor.getInstance()
+        .getConfig()
+        .setDriverTaskExecutionTimeSliceInMs(originalTimeSlice);
+  }
+
+  private enum QueryMode {
+    SHARED,
+    BATCH,
+    DEVICE_SCAN,
+    SERIES_SCAN,
+    FAKED_FAILURE,
+    BATCH_FAILURE
+  }
+
+  private TsFileResource resource(int id) {
+    TsFileResource resource = new TsFileResource(new File(folder.getRoot(), id 
+ "-1-0-0.tsfile"));
+    resource.setStatusForTest(TsFileResourceStatus.NORMAL);
+    return resource;
+  }
+
+  @Test
+  public void sharedQueryReleasesDeletedFilesOutsideRegionLock() throws 
Exception {
+    releasesOutsideRegionLock(QueryMode.SHARED);
+  }
+
+  @Test
+  public void batchQueryReleasesDeletedFilesOutsideRegionLock() throws 
Exception {
+    releasesOutsideRegionLock(QueryMode.BATCH);
+  }
+
+  @Test
+  public void deviceScanReleasesDeletedFilesOutsideRegionLock() throws 
Exception {
+    releasesOutsideRegionLock(QueryMode.DEVICE_SCAN);
+  }
+
+  @Test
+  public void seriesScanReleasesDeletedFilesOutsideRegionLock() throws 
Exception {
+    releasesOutsideRegionLock(QueryMode.SERIES_SCAN);
+  }
+
+  @Test
+  public void fakedQueryRollsBackOutsideRegionLock() throws Exception {
+    releasesOutsideRegionLock(QueryMode.FAKED_FAILURE);
+  }
+
+  @Test
+  public void failedBatchReleasesAllReferencesOutsideRegionLock() throws 
Exception {
+    releasesOutsideRegionLock(QueryMode.BATCH_FAILURE);
+  }
+
+  private void releasesOutsideRegionLock(QueryMode mode) throws Exception {
+    DataRegion region = spy(new DataRegion("root.region_release", "1"));
+    FakedFragmentInstanceContext context = new 
FakedFragmentInstanceContext(null, region);
+    IFullPath path = mock(IFullPath.class);
+    TsFileResource resource = resource(1);
+    if (mode != QueryMode.FAKED_FAILURE) {
+      resource.setStatusForTest(TsFileResourceStatus.DELETED);
+    }
+    TsFileResource failing = spy(resource(2));
+    TsFileResource retained = resource(3);
+    RuntimeException failure = new IllegalStateException("injected reference 
registration failure");
+    doThrow(failure).when(failing).getTsFileID();
+    ArrayList<TsFileResource> seq = new 
ArrayList<>(Collections.singletonList(resource));
+    if (mode == QueryMode.FAKED_FAILURE || mode == QueryMode.BATCH_FAILURE) {
+      seq.add(failing);
+    }
+    if (mode == QueryMode.BATCH_FAILURE) {
+      seq.add(0, retained);
+    }
+    QueryDataSource dataSource = new QueryDataSource(seq, new ArrayList<>());
+    doReturn(dataSource).when(region).query(anyList(), any(), any(), any(), 
any(), anyLong());
+    IFileScanHandle handle = mock(IFileScanHandle.class);
+    when(handle.getTsResource()).thenReturn(resource);
+    when(handle.isClosed()).thenReturn(true);
+    QueryDataSourceForRegionScan scanSource =
+        new QueryDataSourceForRegionScan(
+            new ArrayList<>(Collections.singletonList(handle)), new 
ArrayList<>());
+    doReturn(scanSource)
+        .when(region)
+        .queryForDeviceRegionScan(any(), any(), any(), any(), anyLong());
+    doReturn(scanSource)
+        .when(region)
+        .queryForSeriesRegionScan(anyList(), any(), any(), any(), anyLong());
+
+    TsFileSequenceReader reader = mock(TsFileSequenceReader.class);
+    manager.setReaderForTest(resource.getTsFileID(), true, reader);
+    manager.increaseFileReaderReference(resource, true);
+    CountDownLatch closing = new CountDownLatch(1);
+    CountDownLatch finishClose = new CountDownLatch(1);
+    AtomicReference<Thread> closeThread = new AtomicReference<>();
+    AtomicReference<Thread> queryThread = new AtomicReference<>();
+    CountDownLatch queryStarted = new CountDownLatch(1);
+    doAnswer(
+            invocation -> {
+              closeThread.set(Thread.currentThread());
+              closing.countDown();
+              assertTrue(finishClose.await(20, TimeUnit.SECONDS));
+              return null;
+            })
+        .when(reader)
+        .close();
+    Future<?> release = workers.submit(() -> 
manager.decreaseFileReaderReference(resource, true));
+    Future<?> query = null;
+    try {
+      assertTrue(closing.await(10, TimeUnit.SECONDS));
+      query =
+          workers.submit(
+              () -> {
+                queryThread.set(Thread.currentThread());
+                queryStarted.countDown();
+                switch (mode) {
+                  case SHARED:
+                    
assertTrue(context.initQueryDataSource(Collections.singletonList(path)));
+                    break;
+                  case BATCH:
+                    try (QueryDataSourceLease lease =
+                        
context.initBatchQueryDataSource(Collections.singletonList(path))) {
+                      
assertTrue(lease.getDataSource().getSeqResources().isEmpty());
+                    }
+                    break;
+                  case DEVICE_SCAN:
+                    
assertTrue(context.initRegionScanQueryDataSource(Collections.emptyMap()));
+                    break;
+                  case SERIES_SCAN:
+                    assertTrue(
+                        
context.initRegionScanQueryDataSource(Collections.singletonList(path)));
+                    break;
+                  case FAKED_FAILURE:
+                    assertSame(
+                        failure,
+                        assertThrows(
+                            RuntimeException.class, () -> 
context.initQueryDataSource(path)));
+                    break;
+                  case BATCH_FAILURE:
+                    assertSame(
+                        failure,
+                        assertThrows(
+                            RuntimeException.class,
+                            () ->
+                                
context.initBatchQueryDataSource(Collections.singletonList(path))));
+                    break;
+                  default:
+                    throw new AssertionError(mode);
+                }
+                return null;
+              });
+      assertTrue(queryStarted.await(10, TimeUnit.SECONDS));
+      awaitEntryWait(queryThread.get(), closeThread.get());
+      Future<?> writer =
+          workers.submit(
+              () -> {
+                region.writeLock("deferred reference release test");
+                region.writeUnlock();
+              });
+      writer.get(10, TimeUnit.SECONDS);
+      assertFalse(query.isDone());
+    } finally {
+      finishClose.countDown();
+      release.get(10, TimeUnit.SECONDS);
+      if (query != null) {
+        query.get(10, TimeUnit.SECONDS);
+      }
+    }
+    verify(reader).close();
+    if (mode == QueryMode.SHARED || mode == QueryMode.BATCH) {
+      assertTrue(dataSource.getSeqResources().isEmpty());
+    } else if (mode == QueryMode.DEVICE_SCAN || mode == QueryMode.SERIES_SCAN) 
{
+      assertTrue(scanSource.getSeqFileScanHandles().isEmpty());
+    }
+    for (TsFileResource file : Arrays.asList(resource, failing, retained)) {
+      assertTrue(file.tryWriteLock());
+      file.writeUnlock();
+    }
+    assertTrue(manager.getClosedFileReaderMap().isEmpty());
+    assertTrue(manager.getUnclosedFileReaderMap().isEmpty());
+    if (mode == QueryMode.FAKED_FAILURE) {
+      context.releaseSharedQueryDataSource();
+    }
+  }
+
+  private static void awaitEntryWait(Thread waiter, Thread owner) throws 
Exception {
+    long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+    while (System.nanoTime() < deadline) {
+      ThreadInfo info = 
ManagementFactory.getThreadMXBean().getThreadInfo(waiter.getId());
+      if (info != null
+          && info.getThreadState() == Thread.State.BLOCKED
+          && info.getLockOwnerId() == owner.getId()) {
+        return;
+      }
+      Thread.sleep(1);
+    }
+    fail("query did not reach the entry held by the closing reader");
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/RegionScanTimeSliceTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/RegionScanTimeSliceTest.java
new file mode 100644
index 00000000000..23db46d121e
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/RegionScanTimeSliceTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.conf.CommonDescriptor;
+import org.apache.iotdb.commons.path.IFullPath;
+import org.apache.iotdb.db.storageengine.dataregion.DataRegion;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+public class RegionScanTimeSliceTest {
+  private int originalTimeSlice;
+
+  @Before
+  public void setUp() {
+    originalTimeSlice =
+        
CommonDescriptor.getInstance().getConfig().getDriverTaskExecutionTimeSliceInMs();
+  }
+
+  @After
+  public void tearDown() {
+    CommonDescriptor.getInstance()
+        .getConfig()
+        .setDriverTaskExecutionTimeSliceInMs(originalTimeSlice);
+  }
+
+  @Test
+  public void exhaustedSeriesScanTimeSliceStillUnlocksRegion() throws 
Exception {
+    DataRegion realRegion = new DataRegion("root.region_release", "1");
+    DataRegion region = spy(realRegion);
+    FakedFragmentInstanceContext context = new 
FakedFragmentInstanceContext(null, region);
+    
CommonDescriptor.getInstance().getConfig().setDriverTaskExecutionTimeSliceInMs(1);
+    AtomicBoolean locked = new AtomicBoolean();
+    doAnswer(
+            invocation -> {
+              assertTrue(realRegion.tryReadLock(10_000));
+              locked.set(true);
+              Uninterruptibles.sleepUninterruptibly(10, TimeUnit.MILLISECONDS);
+              return true;
+            })
+        .when(region)
+        .tryReadLock(anyLong());
+    doAnswer(
+            invocation -> {
+              invocation.callRealMethod();
+              locked.set(false);
+              return null;
+            })
+        .when(region)
+        .readUnlock();
+    try {
+      assertFalse(
+          
context.initRegionScanQueryDataSource(Collections.singletonList(mock(IFullPath.class))));
+      assertFalse(
+          "region read lock must be released on the exhausted-time-slice 
return", locked.get());
+      verify(region).readUnlock();
+      assertEquals(0, context.getInitQueryDataSourceRetryCount());
+    } finally {
+      if (locked.get()) {
+        region.readUnlock();
+      }
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/buffer/TimeSeriesMetadataCacheTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/buffer/TimeSeriesMetadataCacheTest.java
index 751eae090ae..87526eb594b 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/buffer/TimeSeriesMetadataCacheTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/buffer/TimeSeriesMetadataCacheTest.java
@@ -238,7 +238,7 @@ public class TimeSeriesMetadataCacheTest {
           new TimeSeriesMetadataCacheKey(tsFileID, deviceID, measurement);
 
       TsFileSequenceReader reader = Mockito.mock(TsFileSequenceReader.class);
-      fileReaderManager.getClosedFileReaderMap().put(tsFileID, reader);
+      fileReaderManager.setReaderForTest(tsFileID, true, reader);
       
Mockito.when(reader.readBloomFilter(Mockito.any(LongConsumer.class))).thenReturn(null);
       Mockito.when(
               reader.readTimeseriesMetadata(
@@ -274,7 +274,7 @@ public class TimeSeriesMetadataCacheTest {
     } finally {
       cache.clear();
       bloomFilterCache.clear();
-      fileReaderManager.getClosedFileReaderMap().remove(tsFileID);
+      fileReaderManager.closeFileAndRemoveReader(tsFileID);
       IoTDBDescriptor.getInstance()
           .getMemoryConfig()
           .setMayCacheNonExistSeries(mayCacheNonExistSeries);
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/read/control/FileReaderManagerConcurrencyTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/read/control/FileReaderManagerConcurrencyTest.java
new file mode 100644
index 00000000000..67a480f43d2
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/read/control/FileReaderManagerConcurrencyTest.java
@@ -0,0 +1,871 @@
+/*
+ * 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.storageengine.dataregion.read.control;
+
+import org.apache.iotdb.db.storageengine.dataregion.DataRegion;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileID;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+
+import org.apache.tsfile.fileSystem.FSFactoryProducer;
+import org.apache.tsfile.fileSystem.fileInputFactory.FileInputFactory;
+import org.apache.tsfile.read.TsFileSequenceReader;
+import org.apache.tsfile.read.reader.TsFileInput;
+import org.apache.tsfile.write.writer.TsFileIOWriter;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class FileReaderManagerConcurrencyTest {
+  @Rule public TemporaryFolder folder = new TemporaryFolder();
+  private final FileReaderManager manager = FileReaderManager.getInstance();
+  private ExecutorService workers;
+
+  @Before
+  public void setUp() throws Exception {
+    manager.closeAndRemoveAllOpenedReaders();
+    workers = Executors.newCachedThreadPool();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    workers.shutdownNow();
+    assertTrue(workers.awaitTermination(15, TimeUnit.SECONDS));
+    manager.closeAndRemoveAllOpenedReaders();
+    assertEmpty();
+  }
+
+  private TsFileResource file(int id) throws Exception {
+    File f = new File(folder.getRoot(), id + "-1-0-0.tsfile");
+    try (TsFileIOWriter writer = new TsFileIOWriter(f)) {
+      writer.endFile();
+    }
+    return new TsFileResource(f);
+  }
+
+  private TsFileSequenceReader get(TsFileResource file, boolean closed) throws 
IOException {
+    return manager.get(file.getTsFilePath(), file.getTsFileID(), closed);
+  }
+
+  private static void done(Future<?> task) throws Exception {
+    task.get(10, TimeUnit.SECONDS);
+  }
+
+  private static class Gate implements AutoCloseable {
+    final CountDownLatch entered = new CountDownLatch(1);
+    final CountDownLatch open = new CountDownLatch(1);
+    final AtomicReference<Thread> owner = new AtomicReference<>();
+
+    void block() {
+      owner.set(Thread.currentThread());
+      entered.countDown();
+      boolean interrupted = false;
+      try {
+        while (true) {
+          try {
+            if (!open.await(20, TimeUnit.SECONDS)) {
+              throw new AssertionError("gate not released");
+            }
+            return;
+          } catch (InterruptedException e) {
+            interrupted = true;
+          }
+        }
+      } finally {
+        if (interrupted) {
+          Thread.currentThread().interrupt();
+        }
+      }
+    }
+
+    void await() throws Exception {
+      assertTrue(entered.await(10, TimeUnit.SECONDS));
+    }
+
+    @Override
+    public void close() {
+      open.countDown();
+    }
+  }
+
+  // Prove lock contention and its owner, rather than inferring it from a 
Future timeout.
+  private static void blocked(Thread waiter, Thread owner) throws Exception {
+    long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+    while (System.nanoTime() < deadline) {
+      ThreadInfo info = 
ManagementFactory.getThreadMXBean().getThreadInfo(waiter.getId());
+      if (info != null
+          && info.getThreadState() == Thread.State.BLOCKED
+          && info.getLockOwnerId() == owner.getId()) {
+        assertNotNull(info.getLockInfo());
+        return;
+      }
+      Thread.sleep(1);
+    }
+    fail("worker did not block on the expected monitor owner");
+  }
+
+  @Test
+  public void slowClosedOpenDoesNotBlockOtherFiles() throws Exception {
+    slowOpen(true, false);
+  }
+
+  @Test
+  public void slowUnclosedOpenDoesNotBlockOtherFiles() throws Exception {
+    slowOpen(false, false);
+  }
+
+  @Test
+  public void slowExternalOpenDoesNotBlockOtherFiles() throws Exception {
+    slowOpen(true, true);
+  }
+
+  private void slowOpen(boolean closed, boolean external) throws Exception {
+    TsFileResource a = file(1);
+    TsFileResource b = file(2);
+    TsFileResource c = file(3);
+    assertNotEquals(a.getTsFileID(), b.getTsFileID());
+    TsFileSequenceReader warm = get(b, true);
+    try (Gate gate = new Gate()) {
+      Future<TsFileSequenceReader> opening =
+          workers.submit(
+              () ->
+                  manager.get(
+                      a.getTsFilePath(), a.getTsFileID(), closed, n -> 
gate.block(), external));
+      gate.await();
+      AtomicReference<Thread> waiter = new AtomicReference<>();
+      CountDownLatch started = new CountDownLatch(1);
+      Future<TsFileSequenceReader> same =
+          workers.submit(
+              () -> {
+                waiter.set(Thread.currentThread());
+                started.countDown();
+                return manager.get(a.getTsFilePath(), a.getTsFileID(), closed, 
null, external);
+              });
+      assertTrue(started.await(10, TimeUnit.SECONDS));
+      blocked(waiter.get(), gate.owner.get());
+      unrelated(b, c, warm);
+      gate.close();
+      assertSame(opening.get(10, TimeUnit.SECONDS), same.get(10, 
TimeUnit.SECONDS));
+    }
+  }
+
+  private void unrelated(TsFileResource b, TsFileResource c, 
TsFileSequenceReader warm)
+      throws Exception {
+    done(
+        workers.submit(
+            () -> {
+              assertSame(warm, get(b, true));
+              assertNotNull(get(c, true));
+              assertNotNull(get(c, false));
+              assertNotNull(manager.get(c.getTsFilePath(), c.getTsFileID(), 
true, null, true));
+              manager.increaseFileReaderReference(b, true);
+              manager.decreaseFileReaderReference(b, true);
+              manager.increaseExternalFileReaderReference(c.getTsFilePath());
+              manager.decreaseExternalFileReaderReference(c.getTsFilePath());
+              return null;
+            }));
+  }
+
+  @Test
+  public void slowForceCloseDoesNotBlockOtherFiles() throws Exception {
+    slowClose(false);
+  }
+
+  @Test
+  public void slowLastReleaseDoesNotBlockOtherFiles() throws Exception {
+    slowClose(true);
+  }
+
+  private void slowClose(boolean release) throws Exception {
+    TsFileResource a = file(1);
+    TsFileResource b = file(2);
+    TsFileResource c = file(3);
+    TsFileSequenceReader warm = get(b, true);
+    TsFileSequenceReader mock = mock(TsFileSequenceReader.class);
+    manager.setReaderForTest(a.getTsFileID(), true, mock);
+    if (release) {
+      manager.increaseFileReaderReference(a, true);
+    }
+    try (Gate gate = new Gate()) {
+      doAnswer(
+              invocation -> {
+                gate.block();
+                return null;
+              })
+          .when(mock)
+          .close();
+      Future<?> closing =
+          workers.submit(
+              () -> {
+                if (release) {
+                  manager.decreaseFileReaderReference(a, true);
+                } else {
+                  manager.closeFileAndRemoveReader(a.getTsFileID());
+                }
+                return null;
+              });
+      gate.await();
+      AtomicReference<Thread> waiter = new AtomicReference<>();
+      CountDownLatch started = new CountDownLatch(1);
+      Future<?> acquire =
+          workers.submit(
+              () -> {
+                waiter.set(Thread.currentThread());
+                started.countDown();
+                manager.increaseFileReaderReference(a, true);
+                try {
+                  assertNotSame(mock, get(a, true));
+                } finally {
+                  manager.decreaseFileReaderReference(a, true);
+                }
+                return null;
+              });
+      assertTrue(started.await(10, TimeUnit.SECONDS));
+      blocked(waiter.get(), gate.owner.get());
+      unrelated(b, c, warm);
+      gate.close();
+      done(closing);
+      done(acquire);
+      verify(mock, times(1)).close();
+      assertFalse(manager.contains(a, true));
+    }
+  }
+
+  @Test
+  public void failedConstructionPreservesReferencesAndRetries() throws 
Exception {
+    TsFileResource a = file(1);
+    manager.increaseFileReaderReference(a, true);
+    manager.increaseFileReaderReference(a, true);
+    try {
+      manager.get(a.getTsFilePath() + ".missing", a.getTsFileID(), true);
+      fail("missing file opened");
+    } catch (IOException expected) {
+      assertFalse(manager.contains(a, true));
+    }
+    TsFileSequenceReader reader = get(a, true);
+    manager.decreaseFileReaderReference(a, true);
+    assertSame(reader, get(a, true));
+    manager.decreaseFileReaderReference(a, true);
+    assertEmpty();
+  }
+
+  @Test
+  public void closeFailureDoesNotPoisonOverlappingQueries() throws Exception {
+    TsFileResource a = file(1);
+    TsFileSequenceReader reader = mock(TsFileSequenceReader.class);
+    doThrow(new IOException("injected close failure")).when(reader).close();
+    manager.setReaderForTest(a.getTsFileID(), true, reader);
+    manager.increaseFileReaderReference(a, true);
+    manager.decreaseFileReaderReference(a, true);
+    assertTrue(a.tryWriteLock());
+    a.writeUnlock();
+    assertEmpty();
+    // A long-running query registers this file before reaching it. Other 
queries must recover
+    // without waiting for its reference (and all the overlapping queries' 
references) to drain.
+    manager.increaseFileReaderReference(a, true);
+    try {
+      for (int i = 0; i < 5; i++) {
+        manager.increaseFileReaderReference(a, true);
+        try {
+          TsFileSequenceReader reopened = get(a, true);
+          assertNotSame(reader, reopened);
+          assertTrue(reopened.fileSize() > 0);
+        } finally {
+          manager.decreaseFileReaderReference(a, true);
+        }
+      }
+    } finally {
+      manager.decreaseFileReaderReference(a, true);
+    }
+    verify(reader, times(1)).close();
+    assertEmpty();
+  }
+
+  @Test
+  public void failedUnclosedCloseDoesNotLeakAfterSealing() throws Exception {
+    TsFileResource a = file(1);
+    TsFileSequenceReader unclosed = mock(TsFileSequenceReader.class);
+    doThrow(new IOException("injected unclosed close 
failure")).when(unclosed).close();
+    manager.setReaderForTest(a.getTsFileID(), false, unclosed);
+    manager.increaseFileReaderReference(a, false);
+    manager.decreaseFileReaderReference(a, false);
+    assertEmpty();
+    // All subsequent queries address the closed slot, never the failed 
unclosed slot again.
+    manager.increaseFileReaderReference(a, true);
+    assertTrue(get(a, true).fileSize() > 0);
+    manager.decreaseFileReaderReference(a, true);
+    assertEmpty();
+  }
+
+  @Test
+  public void failedExternalCloseDoesNotPoisonNextQuery() throws Exception {
+    TsFileResource a = file(1);
+    FileInputFactory original = FSFactoryProducer.getFileInputFactory();
+    TsFileInput input = spy(original.getTsFileInput(a.getTsFilePath()));
+    AtomicBoolean first = new AtomicBoolean(true);
+    doThrow(new IOException("injected external close 
failure")).when(input).close();
+    FSFactoryProducer.setFileInputFactory(
+        path -> first.getAndSet(false) ? input : 
original.getTsFileInput(path));
+    try {
+      manager.increaseExternalFileReaderReference(a.getTsFilePath());
+      TsFileSequenceReader failed = manager.get(a.getTsFilePath(), null, true, 
null, true);
+      manager.decreaseExternalFileReaderReference(a.getTsFilePath());
+      assertEmpty();
+      manager.increaseExternalFileReaderReference(a.getTsFilePath());
+      TsFileSequenceReader reopened = manager.get(a.getTsFilePath(), null, 
true, null, true);
+      assertNotSame(failed, reopened);
+      assertTrue(reopened.fileSize() > 0);
+      manager.decreaseExternalFileReaderReference(a.getTsFilePath());
+      assertEmpty();
+    } finally {
+      FSFactoryProducer.setFileInputFactory(original);
+      // The injected exception deliberately prevented closing the real input.
+      doCallRealMethod().when(input).close();
+      input.close();
+    }
+  }
+
+  @Test
+  public void slowOpenDoesNotHoldRegionLockDuringReferenceRegistration() 
throws Exception {
+    referenceRegistrationDuringIo(false);
+  }
+
+  @Test
+  public void slowCloseDoesNotHoldRegionLockDuringReferenceRegistration() 
throws Exception {
+    referenceRegistrationDuringIo(true);
+  }
+
+  private void referenceRegistrationDuringIo(boolean close) throws Exception {
+    TsFileResource a = file(1);
+    DataRegion region = new DataRegion("root.reference_registration", "1");
+    try (Gate gate = new Gate()) {
+      Future<?> io;
+      if (close) {
+        TsFileSequenceReader reader = mock(TsFileSequenceReader.class);
+        manager.setReaderForTest(a.getTsFileID(), true, reader);
+        manager.increaseFileReaderReference(a, true);
+        doAnswer(
+                invocation -> {
+                  gate.block();
+                  return null;
+                })
+            .when(reader)
+            .close();
+        io = workers.submit(() -> manager.decreaseFileReaderReference(a, 
true));
+      } else {
+        io =
+            workers.submit(
+                () -> manager.get(a.getTsFilePath(), a.getTsFileID(), true, n 
-> gate.block()));
+      }
+      gate.await();
+      done(
+          workers.submit(
+              () -> {
+                assertTrue(region.tryReadLock(10_000));
+                try {
+                  manager.increaseFileReaderReference(a, true);
+                } finally {
+                  region.readUnlock();
+                }
+              }));
+      // A writer must progress while reader I/O is still blocked on the same 
file.
+      done(
+          workers.submit(
+              () -> {
+                region.writeLock("reference registration test");
+                region.writeUnlock();
+              }));
+      assertFalse(io.isDone());
+      gate.close();
+      done(io);
+      assertTrue(get(a, true).fileSize() > 0);
+      assertFalse(a.tryWriteLock());
+      manager.decreaseFileReaderReference(a, true);
+      assertTrue(a.tryWriteLock());
+      a.writeUnlock();
+      assertEmpty();
+    }
+  }
+
+  @Test
+  public void lastReleaseRechecksReferencesAfterWaitingForOpen() throws 
Exception {
+    TsFileResource a = file(1);
+    manager.increaseFileReaderReference(a, true);
+    try (Gate gate = new Gate()) {
+      Future<TsFileSequenceReader> open =
+          workers.submit(
+              () -> manager.get(a.getTsFilePath(), a.getTsFileID(), true, n -> 
gate.block()));
+      gate.await();
+      AtomicReference<Thread> releaseThread = new AtomicReference<>();
+      CountDownLatch started = new CountDownLatch(1);
+      Future<?> release =
+          workers.submit(
+              () -> {
+                releaseThread.set(Thread.currentThread());
+                started.countDown();
+                manager.decreaseFileReaderReference(a, true);
+              });
+      assertTrue(started.await(10, TimeUnit.SECONDS));
+      blocked(releaseThread.get(), gate.owner.get());
+      done(workers.submit(() -> manager.increaseFileReaderReference(a, true)));
+      gate.close();
+      TsFileSequenceReader reader = open.get(10, TimeUnit.SECONDS);
+      done(release);
+      assertSame(reader, get(a, true));
+      assertTrue(reader.fileSize() > 0);
+      assertFalse(a.tryWriteLock());
+      manager.decreaseFileReaderReference(a, true);
+      assertEmpty();
+    }
+  }
+
+  @Test
+  public void namespacesAndSlotReferencesAreIndependent() throws Exception {
+    TsFileResource a = file(1);
+    manager.increaseFileReaderReference(a, true);
+    manager.increaseFileReaderReference(a, false);
+    manager.increaseExternalFileReaderReference(a.getTsFilePath());
+    TsFileSequenceReader closed = get(a, true);
+    TsFileSequenceReader unclosed = get(a, false);
+    TsFileSequenceReader external =
+        manager.get(a.getTsFilePath(), a.getTsFileID(), true, null, true);
+    assertNotSame(closed, unclosed);
+    assertNotSame(closed, external);
+    assertSame(external, manager.get(new String(a.getTsFilePath()), null, 
false, null, true));
+    manager.decreaseFileReaderReference(a, true);
+    assertFalse(manager.contains(a, true));
+    assertSame(unclosed, get(a, false));
+    assertTrue(unclosed.fileSize() > 0);
+    manager.decreaseFileReaderReference(a, false);
+    assertFalse(manager.contains(a, false));
+    // Forced internal-reader cleanup must not affect the separately retained 
external reader.
+    assertNotNull(get(a, true));
+    assertNotNull(get(a, false));
+    manager.closeFileAndRemoveReader(a.getTsFileID());
+    assertFalse(manager.contains(a, true));
+    assertFalse(manager.contains(a, false));
+    assertSame(external, manager.get(a.getTsFilePath(), null, true, null, 
true));
+    assertTrue(external.fileSize() > 0);
+    manager.decreaseExternalFileReaderReference(a.getTsFilePath());
+    assertEmpty();
+  }
+
+  @Test
+  public void lateUnclosedReleaseDoesNotCloseNewClosedReader() throws 
Exception {
+    TsFileResource a = file(1);
+    manager.increaseFileReaderReference(a, false);
+    assertNotNull(get(a, false));
+    // Reader use has stopped, but this query still owes its resource 
read-lock release.
+    manager.closeAndRemoveAllOpenedReaders();
+    manager.increaseFileReaderReference(a, true);
+    try {
+      TsFileSequenceReader closed = get(a, true);
+      manager.decreaseFileReaderReference(a, false);
+      // The old unclosed reference was already cleared. Its release must not 
consume the new
+      // query's closed reference or close that query's reader.
+      assertTrue(manager.contains(a, true));
+      assertSame(closed, get(a, true));
+      assertTrue(closed.fileSize() > 0);
+      assertFalse(a.tryWriteLock());
+    } finally {
+      manager.decreaseFileReaderReference(a, true);
+    }
+    assertTrue(a.tryWriteLock());
+    a.writeUnlock();
+    assertEmpty();
+  }
+
+  @Test
+  public void clearDrainsReleasePinsEvenWhenInterrupted() throws Exception {
+    TsFileResource a = file(1);
+    TsFileSequenceReader reader = mock(TsFileSequenceReader.class);
+    manager.setReaderForTest(a.getTsFileID(), true, reader);
+    Object registry = field("internal");
+    Method pin =
+        FileReaderManager.class.getDeclaredMethod(
+            "pin", Map.class, Object.class, boolean.class, boolean.class);
+    pin.setAccessible(true);
+    Object entry = null;
+    try (Gate gate = new Gate()) {
+      doAnswer(
+              invocation -> {
+                gate.block();
+                return null;
+              })
+          .when(reader)
+          .close();
+      Future<Boolean> clear =
+          workers.submit(
+              () -> {
+                manager.closeAndRemoveAllOpenedReaders();
+                return Thread.interrupted();
+              });
+      gate.await();
+      // Hold a release pin beyond the close phase: a deterministic model of a 
descheduled release.
+      entry = pin.invoke(manager, registry, a.getTsFileID(), false, false);
+      assertNotNull(entry);
+      gate.owner.get().interrupt();
+      Future<?> acquire = acquireDuringClear(a, true);
+      gate.close();
+      awaitClearDrain(gate.owner.get());
+      assertFalse(clear.isDone());
+      assertFalse(acquire.isDone());
+      Method unpin =
+          FileReaderManager.class.getDeclaredMethod(
+              "unpin", Map.class, Object.class, entry.getClass());
+      unpin.setAccessible(true);
+      unpin.invoke(manager, registry, a.getTsFileID(), entry);
+      entry = null;
+      assertTrue(clear.get(10, TimeUnit.SECONDS));
+      done(acquire);
+    } finally {
+      if (entry != null) {
+        Method unpin =
+            FileReaderManager.class.getDeclaredMethod(
+                "unpin", Map.class, Object.class, entry.getClass());
+        unpin.setAccessible(true);
+        unpin.invoke(manager, registry, a.getTsFileID(), entry);
+      }
+    }
+  }
+
+  private Future<TsFileSequenceReader> acquireDuringClear(TsFileResource 
resource, boolean closed)
+      throws Exception {
+    AtomicReference<Thread> thread = new AtomicReference<>();
+    CountDownLatch started = new CountDownLatch(1);
+    Future<TsFileSequenceReader> acquire =
+        workers.submit(
+            () -> {
+              thread.set(Thread.currentThread());
+              started.countDown();
+              return get(resource, closed);
+            });
+    assertTrue(started.await(10, TimeUnit.SECONDS));
+    // An unfinished Future alone does not prove that the worker has reached 
the admission gate.
+    awaitRegistryWait(thread.get(), "pin");
+    return acquire;
+  }
+
+  private void awaitClearDrain(Thread thread) throws Exception {
+    awaitRegistryWait(thread, "drainPins");
+  }
+
+  private void awaitRegistryWait(Thread thread, String method) throws 
Exception {
+    Object registryLock = field("registryLock");
+    long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+    while (System.nanoTime() < deadline) {
+      ThreadInfo info = 
ManagementFactory.getThreadMXBean().getThreadInfo(thread.getId(), 30);
+      if (info != null
+          && info.getThreadState() == Thread.State.WAITING
+          && info.getLockInfo() != null
+          && info.getLockInfo().getIdentityHashCode() == 
System.identityHashCode(registryLock)) {
+        for (StackTraceElement frame : info.getStackTrace()) {
+          if (frame.getClassName().equals(FileReaderManager.class.getName())
+              && frame.getMethodName().equals(method)) {
+            return;
+          }
+        }
+      }
+      Thread.sleep(1);
+    }
+    fail("worker did not wait on registryLock in " + method);
+  }
+
+  @Test
+  public void concurrentClearAndReleaseComplete() throws Exception {
+    TsFileResource a = file(1);
+    TsFileSequenceReader reader = mock(TsFileSequenceReader.class);
+    manager.setReaderForTest(a.getTsFileID(), true, reader);
+    manager.increaseFileReaderReference(a, true);
+    try (Gate gate = new Gate()) {
+      doAnswer(
+              invocation -> {
+                gate.block();
+                return null;
+              })
+          .when(reader)
+          .close();
+      Future<?> first =
+          workers.submit(
+              () -> {
+                manager.closeAndRemoveAllOpenedReaders();
+                return null;
+              });
+      gate.await();
+      AtomicReference<Thread> releaseThread = new AtomicReference<>();
+      CountDownLatch started = new CountDownLatch(1);
+      Future<?> release =
+          workers.submit(
+              () -> {
+                releaseThread.set(Thread.currentThread());
+                started.countDown();
+                manager.decreaseFileReaderReference(a, true);
+              });
+      assertTrue(started.await(10, TimeUnit.SECONDS));
+      // Clear already reset the reference count. This release no longer waits 
for reader I/O.
+      done(release);
+      AtomicReference<Thread> secondThread = new AtomicReference<>();
+      CountDownLatch secondStarted = new CountDownLatch(1);
+      Future<?> second =
+          workers.submit(
+              () -> {
+                secondThread.set(Thread.currentThread());
+                secondStarted.countDown();
+                manager.closeAndRemoveAllOpenedReaders();
+                return null;
+              });
+      assertTrue(secondStarted.await(10, TimeUnit.SECONDS));
+      blocked(secondThread.get(), gate.owner.get());
+      gate.close();
+      done(first);
+      done(second);
+      done(release);
+      verify(reader, times(1)).close();
+      assertEmpty();
+    }
+  }
+
+  @Test
+  public void concurrentChurnReclaimsEntriesAndReaders() throws Exception {
+    TsFileResource[] files = {file(1), file(2), file(3)};
+    List<Future<?>> tasks = new ArrayList<>();
+    for (int t = 0; t < 8; t++) {
+      tasks.add(
+          workers.submit(
+              () -> {
+                for (int i = 0; i < 500; i++) {
+                  TsFileResource f = files[i % files.length];
+                  boolean closed = (i & 1) == 0;
+                  manager.increaseFileReaderReference(f, closed);
+                  try {
+                    assertSame(get(f, closed), get(f, closed));
+                  } finally {
+                    manager.decreaseFileReaderReference(f, closed);
+                  }
+                  
manager.increaseExternalFileReaderReference(f.getTsFilePath());
+                  try {
+                    assertNotNull(manager.get(f.getTsFilePath(), null, true, 
null, true));
+                  } finally {
+                    
manager.decreaseExternalFileReaderReference(f.getTsFilePath());
+                  }
+                }
+                return null;
+              }));
+    }
+    for (Future<?> task : tasks) {
+      task.get(60, TimeUnit.SECONDS);
+    }
+    assertEmpty();
+  }
+
+  @Test
+  public void forceCloseAttemptsBothSlotsAndDiscardsFailure() throws Exception 
{
+    TsFileResource a = file(1);
+    TsFileSequenceReader closed = mock(TsFileSequenceReader.class);
+    TsFileSequenceReader unclosed = mock(TsFileSequenceReader.class);
+    manager.setReaderForTest(a.getTsFileID(), true, closed);
+    manager.setReaderForTest(a.getTsFileID(), false, unclosed);
+    doThrow(new IOException("close failed")).doNothing().when(closed).close();
+    try {
+      manager.closeFileAndRemoveReader(a.getTsFileID());
+      fail("close failure lost");
+    } catch (IOException expected) {
+      verify(unclosed).close();
+      assertFalse(manager.contains(a, false));
+    }
+    manager.closeAndRemoveAllOpenedReaders();
+    verify(closed, times(1)).close();
+    assertEmpty();
+  }
+
+  @Test
+  public void closeAggregationDoesNotMutateReaderExceptions() throws Exception 
{
+    TsFileResource a = file(1);
+    TsFileSequenceReader closed = mock(TsFileSequenceReader.class);
+    TsFileSequenceReader unclosed = mock(TsFileSequenceReader.class);
+    IOException first = new IOException("first close failure");
+    IOException second = new IOException("second close failure");
+    doThrow(first).when(closed).close();
+    doThrow(second).when(unclosed).close();
+    manager.setReaderForTest(a.getTsFileID(), true, closed);
+    manager.setReaderForTest(a.getTsFileID(), false, unclosed);
+    IOException aggregate =
+        assertThrows(IOException.class, () -> 
manager.closeFileAndRemoveReader(a.getTsFileID()));
+    assertSame(first, aggregate.getCause());
+    assertSame(second, aggregate.getSuppressed()[0]);
+    assertEquals(0, first.getSuppressed().length);
+    assertEquals(0, second.getSuppressed().length);
+    assertEmpty();
+  }
+
+  @Test
+  public void clearReopensAdmissionAfterSnapshotOrReclamationError() throws 
Exception {
+    Field internal = FileReaderManager.class.getDeclaredField("internal");
+    internal.setAccessible(true);
+    Object original = internal.get(manager);
+    for (int failureCall : new int[] {1, 2}) {
+      Map<TsFileID, Object> failing =
+          new HashMap<TsFileID, Object>() {
+            private int calls;
+
+            @Override
+            public Collection<Object> values() {
+              if (++calls == failureCall) {
+                throw new AssertionError("injected registry traversal 
failure");
+              }
+              return super.values();
+            }
+          };
+      internal.set(manager, failing);
+      try {
+        assertThrows(AssertionError.class, 
manager::closeAndRemoveAllOpenedReaders);
+        assertEquals(false, field("clearing"));
+      } finally {
+        internal.set(manager, original);
+      }
+    }
+    TsFileResource a = file(1);
+    assertNotNull(get(a, true));
+  }
+
+  @Test
+  public void pinnedEmptyEntryCannotBeReplaced() throws Exception {
+    TsFileResource a = file(1);
+    Object registry = field("internal");
+    Method pin =
+        FileReaderManager.class.getDeclaredMethod(
+            "pin", Map.class, Object.class, boolean.class, boolean.class);
+    pin.setAccessible(true);
+    Object entry = pin.invoke(manager, registry, a.getTsFileID(), true, true);
+    try {
+      manager.closeFileAndRemoveReader(a.getTsFileID());
+      done(workers.submit(() -> get(a, true)));
+      assertSame(entry, ((Map<?, ?>) registry).get(a.getTsFileID()));
+      manager.closeFileAndRemoveReader(a.getTsFileID());
+      assertSame(entry, ((Map<?, ?>) registry).get(a.getTsFileID()));
+    } finally {
+      Method unpin =
+          FileReaderManager.class.getDeclaredMethod(
+              "unpin", Map.class, Object.class, entry.getClass());
+      unpin.setAccessible(true);
+      unpin.invoke(manager, registry, a.getTsFileID(), entry);
+    }
+    assertEmpty();
+  }
+
+  @Test
+  public void failedRegistrationReleasesResourceLock() throws Exception {
+    TsFileResource resource = mock(TsFileResource.class);
+    org.mockito.Mockito.when(resource.getTsFileID())
+        .thenThrow(new IllegalStateException("injected"));
+    try {
+      manager.increaseFileReaderReference(resource, true);
+      fail("registration failure lost");
+    } catch (IllegalStateException expected) {
+      verify(resource).readLock();
+      verify(resource).readUnlock();
+    }
+    assertEmpty();
+  }
+
+  @Test
+  public void interruptedClearWaitsForExistingOperation() throws Exception {
+    TsFileResource a = file(1);
+    try (Gate gate = new Gate()) {
+      Future<?> open =
+          workers.submit(
+              () -> manager.get(a.getTsFilePath(), a.getTsFileID(), true, n -> 
gate.block()));
+      gate.await();
+      AtomicReference<Thread> clearThread = new AtomicReference<>();
+      CountDownLatch started = new CountDownLatch(1);
+      Future<Boolean> clear =
+          workers.submit(
+              () -> {
+                clearThread.set(Thread.currentThread());
+                started.countDown();
+                manager.closeAndRemoveAllOpenedReaders();
+                return Thread.interrupted();
+              });
+      assertTrue(started.await(10, TimeUnit.SECONDS));
+      awaitClearDrain(clearThread.get());
+      clearThread.get().interrupt();
+      Future<?> acquire = acquireDuringClear(a, false);
+      assertFalse(clear.isDone());
+      assertFalse(acquire.isDone());
+      gate.close();
+      done(open);
+      assertTrue(clear.get(10, TimeUnit.SECONDS));
+      done(acquire);
+      assertFalse(manager.contains(a, true));
+      assertTrue(manager.contains(a, false));
+    }
+  }
+
+  private Object field(String name) throws Exception {
+    Field field = FileReaderManager.class.getDeclaredField(name);
+    field.setAccessible(true);
+    return field.get(manager);
+  }
+
+  private void assertEmpty() throws Exception {
+    assertEquals(0, field("pins"));
+    assertTrue(((Map<?, ?>) field("internal")).isEmpty());
+    assertTrue(((Map<?, ?>) field("external")).isEmpty());
+    for (String name : new String[] {"closedCount", "unclosedCount", 
"externalCount"}) {
+      assertEquals(0, ((java.util.concurrent.atomic.AtomicInteger) 
field(name)).get());
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/tsfile/TsFileLockTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/tsfile/TsFileLockTest.java
new file mode 100644
index 00000000000..d574070003a
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/tsfile/TsFileLockTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.storageengine.dataregion.tsfile;
+
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class TsFileLockTest {
+  @Test
+  public void interruptedWriterStillWaitsForReaders() throws Exception {
+    interruptedWaiter(true);
+  }
+
+  @Test
+  public void interruptedReaderStillWaitsForWriter() throws Exception {
+    interruptedWaiter(false);
+  }
+
+  private void interruptedWaiter(boolean writer) throws Exception {
+    TsFileLock lock = new TsFileLock();
+    ExecutorService worker = Executors.newSingleThreadExecutor();
+    AtomicReference<Thread> waiter = new AtomicReference<>();
+    CountDownLatch started = new CountDownLatch(1);
+    if (writer) {
+      lock.readLock();
+    } else {
+      lock.writeLock();
+    }
+    boolean held = true;
+    try {
+      Future<Boolean> acquired =
+          worker.submit(
+              () -> {
+                waiter.set(Thread.currentThread());
+                started.countDown();
+                if (writer) {
+                  lock.writeLock();
+                } else {
+                  lock.readLock();
+                }
+                try {
+                  return Thread.currentThread().isInterrupted();
+                } finally {
+                  if (writer) {
+                    lock.writeUnlock();
+                  } else {
+                    lock.readUnlock();
+                  }
+                }
+              });
+      assertTrue(started.await(10, TimeUnit.SECONDS));
+      long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10);
+      while (waiter.get().getState() != Thread.State.WAITING
+          && waiter.get().getState() != Thread.State.TIMED_WAITING) {
+        if (System.nanoTime() > deadline) {
+          fail("waiter never reached the resource lock");
+        }
+        Thread.sleep(1);
+      }
+      waiter.get().interrupt();
+      assertThrows(TimeoutException.class, () -> acquired.get(100, 
TimeUnit.MILLISECONDS));
+      if (writer) {
+        lock.readUnlock();
+      } else {
+        lock.writeUnlock();
+      }
+      held = false;
+      assertTrue(acquired.get(10, TimeUnit.SECONDS));
+    } finally {
+      if (held) {
+        if (writer) {
+          lock.readUnlock();
+        } else {
+          lock.writeUnlock();
+        }
+      }
+      worker.shutdownNow();
+      assertTrue(worker.awaitTermination(10, TimeUnit.SECONDS));
+    }
+  }
+}

Reply via email to