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

jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/master by this push:
     new c98bc5a0327 Use MinFolderOccupiedSpaceFirstStrategy for IoTConsensus 
recv and all… (#18020)
c98bc5a0327 is described below

commit c98bc5a0327c0308a40a7c63df94301fd358f360
Author: Hongzhi Gao <[email protected]>
AuthorDate: Thu Jun 25 14:48:49 2026 +0800

    Use MinFolderOccupiedSpaceFirstStrategy for IoTConsensus recv and all… 
(#18020)
    
    * Use MinFolderOccupiedSpaceFirstStrategy for IoTConsensus recv and allow 
it in cluster
    
    - IoTConsensus snapshot receive folder now always uses
      MinFolderOccupiedSpaceFirstStrategy, balancing received files by least
      occupied space independent of the global dn_multi_dir_strategy.
    - Allow MinFolderOccupiedSpaceFirstStrategy in cluster mode
      (CLUSTER_ALLOWED_MULTI_DIR_STRATEGIES) so it can be configured for 
DataNodes.
    - Fix NPE in the dn_multi_dir_strategy validation rollback path when the
      previous value is null, so the informative RuntimeException surfaces.
    - IT: add DataNodeConfig.setDnMultiDirStrategy and exercise the strategy in
      IoTDBRegionMigrateWithDeletionMultiDataDirIT.
    
    * Fix multi-recv snapshot loading to keep tsfile and mods together
    
    When IoTConsensus spreads snapshot fragments across recv folders, disable 
keepSameDiskWhenLoading so fileTarget groups companion files on one data dir.
    
    * Group snapshot companion files into one receive folder on IoTConsensus 
receiver
    
    receiveSnapshotFragment now selects the receive folder once per TsFile group
    (fileKey = file name before the first '.', matching SnapshotLoader) via a
    per-snapshotId ConcurrentHashMap.computeIfAbsent, so a TsFile and its
    .resource/.mods companions land in the same receive folder and the load 
phase
    relinks them within one data dir instead of falling back to a cross-disk 
copy.
    The mapping is dropped once the snapshot is loaded or cleaned up.
    
    Convert IoTDBRegionMigrateWithDeletionMultiDataDirIT to the tree model and 
add
    the table-model twin IoTDBRegionMigrateWithDeletionMultiDataDirTableIT
    (@TableClusterIT) so both cluster CI profiles cover the snapshot mods 
transfer.
---
 .../it/env/cluster/config/MppDataNodeConfig.java   |  6 ++
 .../it/env/remote/config/RemoteDataNodeConfig.java |  5 ++
 .../apache/iotdb/itbase/env/DataNodeConfig.java    |  2 +
 ...TDBRegionMigrateWithDeletionMultiDataDirIT.java | 48 +++++++-------
 ...ionMigrateWithDeletionMultiDataDirTableIT.java} | 11 +++-
 .../consensus/iot/IoTConsensusServerImpl.java      | 75 ++++++++++++++++++----
 .../java/org/apache/iotdb/db/conf/IoTDBConfig.java |  4 +-
 .../org/apache/iotdb/db/conf/IoTDBDescriptor.java  |  3 +-
 .../db/consensus/DataRegionConsensusImpl.java      |  5 +-
 .../dataregion/snapshot/SnapshotLoader.java        | 20 ++++--
 .../dataregion/snapshot/IoTDBSnapshotTest.java     |  5 +-
 11 files changed, 131 insertions(+), 53 deletions(-)

diff --git 
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppDataNodeConfig.java
 
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppDataNodeConfig.java
index 4d86b481cbc..8399955a5c5 100644
--- 
a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppDataNodeConfig.java
+++ 
b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/config/MppDataNodeConfig.java
@@ -174,4 +174,10 @@ public class MppDataNodeConfig extends MppBaseConfig 
implements DataNodeConfig {
     setProperty("dn_data_dirs", dnDataDirs);
     return this;
   }
+
+  @Override
+  public DataNodeConfig setDnMultiDirStrategy(String multiDirStrategy) {
+    setProperty("dn_multi_dir_strategy", multiDirStrategy);
+    return this;
+  }
 }
diff --git 
a/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteDataNodeConfig.java
 
b/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteDataNodeConfig.java
index 1142054d132..a76608e4851 100644
--- 
a/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteDataNodeConfig.java
+++ 
b/integration-test/src/main/java/org/apache/iotdb/it/env/remote/config/RemoteDataNodeConfig.java
@@ -120,4 +120,9 @@ public class RemoteDataNodeConfig implements DataNodeConfig 
{
   public DataNodeConfig setDnDataDirs(String dnDataDirs) {
     return this;
   }
+
+  @Override
+  public DataNodeConfig setDnMultiDirStrategy(String multiDirStrategy) {
+    return this;
+  }
 }
diff --git 
a/integration-test/src/main/java/org/apache/iotdb/itbase/env/DataNodeConfig.java
 
b/integration-test/src/main/java/org/apache/iotdb/itbase/env/DataNodeConfig.java
index 1df0d54b211..bc045c9ba2f 100644
--- 
a/integration-test/src/main/java/org/apache/iotdb/itbase/env/DataNodeConfig.java
+++ 
b/integration-test/src/main/java/org/apache/iotdb/itbase/env/DataNodeConfig.java
@@ -63,4 +63,6 @@ public interface DataNodeConfig {
   DataNodeConfig setQueryCostStatWindow(int queryCostStatWindow);
 
   DataNodeConfig setDnDataDirs(String dnDataDirs);
+
+  DataNodeConfig setDnMultiDirStrategy(String multiDirStrategy);
 }
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/confignode/it/regionmigration/pass/daily/iotv1/IoTDBRegionMigrateWithDeletionMultiDataDirIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/confignode/it/regionmigration/pass/daily/iotv1/IoTDBRegionMigrateWithDeletionMultiDataDirIT.java
index 43b2d1bc992..8214bf621cc 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/confignode/it/regionmigration/pass/daily/iotv1/IoTDBRegionMigrateWithDeletionMultiDataDirIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/confignode/it/regionmigration/pass/daily/iotv1/IoTDBRegionMigrateWithDeletionMultiDataDirIT.java
@@ -20,12 +20,10 @@
 package org.apache.iotdb.confignode.it.regionmigration.pass.daily.iotv1;
 
 import org.apache.iotdb.consensus.ConsensusFactory;
-import org.apache.iotdb.isession.SessionConfig;
 import org.apache.iotdb.it.env.EnvFactory;
 import org.apache.iotdb.it.env.cluster.node.DataNodeWrapper;
 import org.apache.iotdb.it.framework.IoTDBTestRunner;
 import org.apache.iotdb.itbase.category.ClusterIT;
-import org.apache.iotdb.itbase.env.BaseEnv;
 
 import org.apache.tsfile.utils.Pair;
 import org.awaitility.Awaitility;
@@ -47,6 +45,13 @@ import java.util.concurrent.TimeUnit;
 import static 
org.apache.iotdb.confignode.it.regionmigration.IoTDBRegionOperationReliabilityITFramework.getAllDataNodes;
 import static 
org.apache.iotdb.confignode.it.regionmigration.IoTDBRegionOperationReliabilityITFramework.getDataRegionMapWithLeader;
 
+/**
+ * Tree-model coverage for IoTConsensus region migration over multiple data 
dirs: a deletion (mods)
+ * must survive the snapshot transfer to the migrated peer. With several data 
dirs the snapshot
+ * fragments of one TsFile can be received into different folders, so the 
receiver groups companion
+ * files and the loader relinks them into one data dir; if that breaks, the 
migrated replica loses
+ * the deletion. See the table-model twin {@link 
IoTDBRegionMigrateWithDeletionMultiDataDirTableIT}.
+ */
 @RunWith(IoTDBTestRunner.class)
 @Category({ClusterIT.class})
 public class IoTDBRegionMigrateWithDeletionMultiDataDirIT {
@@ -72,21 +77,20 @@ public class IoTDBRegionMigrateWithDeletionMultiDataDirIT {
 
   @Test
   public void testRegionMigratePreservesDeletionWithMultiDataDirs() throws 
Exception {
-    try (Connection connection = EnvFactory.getEnv().getTableConnection();
+    try (Connection connection = EnvFactory.getEnv().getConnection();
         Statement statement = connection.createStatement()) {
-      statement.execute("CREATE DATABASE test");
-      statement.execute("USE test");
-      statement.execute("CREATE TABLE t1 (s1 INT64 FIELD)");
-      statement.execute("INSERT INTO t1 (time, s1) VALUES (100, 100), (200, 
200), (300, 300)");
+      statement.execute("CREATE DATABASE root.db");
+      statement.execute(
+          "INSERT INTO root.db.d1(timestamp, s1) VALUES (100, 100), (200, 
200), (300, 300)");
       statement.execute("FLUSH");
-      statement.execute("DELETE FROM t1 WHERE time <= 200");
+      statement.execute("DELETE FROM root.db.d1.s1 WHERE time <= 200");
       statement.execute("FLUSH");
 
       Map<Integer, Pair<Integer, Set<Integer>>> dataRegionMapWithLeader =
           getDataRegionMapWithLeader(statement);
       int dataRegionIdForTest =
           
dataRegionMapWithLeader.keySet().stream().max(Integer::compareTo).orElseThrow();
-      assertDeletionVisibleOnAllReplicas(statement, dataRegionIdForTest, 1);
+      assertDeletionVisibleOnAllReplicas(dataRegionIdForTest, 1);
 
       Pair<Integer, Set<Integer>> leaderAndNodes = 
dataRegionMapWithLeader.get(dataRegionIdForTest);
       Set<Integer> allDataNodes = getAllDataNodes(statement);
@@ -123,13 +127,17 @@ public class IoTDBRegionMigrateWithDeletionMultiDataDirIT 
{
                 }
               });
 
-      assertDeletionVisibleOnAllReplicas(statement, dataRegionIdForTest, 1);
+      assertDeletionVisibleOnAllReplicas(dataRegionIdForTest, 1);
     }
   }
 
-  private void assertDeletionVisibleOnAllReplicas(
-      Statement statement, int dataRegionId, int expectedCount) throws 
Exception {
-    Set<Integer> replicaDataNodeIds = getReplicaDataNodeIds(statement, 
dataRegionId);
+  private void assertDeletionVisibleOnAllReplicas(int dataRegionId, int 
expectedCount)
+      throws Exception {
+    Set<Integer> replicaDataNodeIds;
+    try (Connection connection = EnvFactory.getEnv().getConnection();
+        Statement statement = connection.createStatement()) {
+      replicaDataNodeIds = getReplicaDataNodeIds(statement, dataRegionId);
+    }
     for (int dataNodeId : replicaDataNodeIds) {
       DataNodeWrapper dataNodeWrapper =
           EnvFactory.getEnv().dataNodeIdToWrapper(dataNodeId).orElseThrow();
@@ -143,21 +151,15 @@ public class IoTDBRegionMigrateWithDeletionMultiDataDirIT 
{
 
   private void assertDeletionVisibleOnReplica(DataNodeWrapper dataNodeWrapper, 
int expectedCount)
       throws Exception {
-    try (Connection connection =
-            EnvFactory.getEnv()
-                .getConnection(
-                    dataNodeWrapper,
-                    SessionConfig.DEFAULT_USER,
-                    SessionConfig.DEFAULT_PASSWORD,
-                    BaseEnv.TABLE_SQL_DIALECT);
+    try (Connection connection = 
EnvFactory.getEnv().getConnection(dataNodeWrapper);
         Statement dataNodeStatement = connection.createStatement()) {
-      dataNodeStatement.execute("USE test");
-      try (ResultSet countResultSet = dataNodeStatement.executeQuery("SELECT 
COUNT(s1) FROM t1")) {
+      try (ResultSet countResultSet =
+          dataNodeStatement.executeQuery("SELECT COUNT(s1) FROM root.db.d1")) {
         Assert.assertTrue(countResultSet.next());
         Assert.assertEquals(expectedCount, countResultSet.getLong(1));
       }
       try (ResultSet deletedRangeResultSet =
-          dataNodeStatement.executeQuery("SELECT s1 FROM t1 WHERE time <= 
200")) {
+          dataNodeStatement.executeQuery("SELECT s1 FROM root.db.d1 WHERE time 
<= 200")) {
         Assert.assertFalse(deletedRangeResultSet.next());
       }
     }
diff --git 
a/integration-test/src/test/java/org/apache/iotdb/confignode/it/regionmigration/pass/daily/iotv1/IoTDBRegionMigrateWithDeletionMultiDataDirIT.java
 
b/integration-test/src/test/java/org/apache/iotdb/confignode/it/regionmigration/pass/daily/iotv1/IoTDBRegionMigrateWithDeletionMultiDataDirTableIT.java
similarity index 93%
copy from 
integration-test/src/test/java/org/apache/iotdb/confignode/it/regionmigration/pass/daily/iotv1/IoTDBRegionMigrateWithDeletionMultiDataDirIT.java
copy to 
integration-test/src/test/java/org/apache/iotdb/confignode/it/regionmigration/pass/daily/iotv1/IoTDBRegionMigrateWithDeletionMultiDataDirTableIT.java
index 43b2d1bc992..59d5a18ccf4 100644
--- 
a/integration-test/src/test/java/org/apache/iotdb/confignode/it/regionmigration/pass/daily/iotv1/IoTDBRegionMigrateWithDeletionMultiDataDirIT.java
+++ 
b/integration-test/src/test/java/org/apache/iotdb/confignode/it/regionmigration/pass/daily/iotv1/IoTDBRegionMigrateWithDeletionMultiDataDirTableIT.java
@@ -24,7 +24,7 @@ import org.apache.iotdb.isession.SessionConfig;
 import org.apache.iotdb.it.env.EnvFactory;
 import org.apache.iotdb.it.env.cluster.node.DataNodeWrapper;
 import org.apache.iotdb.it.framework.IoTDBTestRunner;
-import org.apache.iotdb.itbase.category.ClusterIT;
+import org.apache.iotdb.itbase.category.TableClusterIT;
 import org.apache.iotdb.itbase.env.BaseEnv;
 
 import org.apache.tsfile.utils.Pair;
@@ -47,9 +47,14 @@ import java.util.concurrent.TimeUnit;
 import static 
org.apache.iotdb.confignode.it.regionmigration.IoTDBRegionOperationReliabilityITFramework.getAllDataNodes;
 import static 
org.apache.iotdb.confignode.it.regionmigration.IoTDBRegionOperationReliabilityITFramework.getDataRegionMapWithLeader;
 
+/**
+ * Table-model twin of {@link IoTDBRegionMigrateWithDeletionMultiDataDirIT}: a 
deletion (mods) must
+ * survive IoTConsensus region migration across multiple data dirs, asserted 
through the relational
+ * (table) SQL dialect so the table-model cluster CI covers the same snapshot 
mods-transfer path.
+ */
 @RunWith(IoTDBTestRunner.class)
-@Category({ClusterIT.class})
-public class IoTDBRegionMigrateWithDeletionMultiDataDirIT {
+@Category({TableClusterIT.class})
+public class IoTDBRegionMigrateWithDeletionMultiDataDirTableIT {
 
   private static final String MULTI_DATA_DIRS =
       
"data/datanode/data/disk0,data/datanode/data/disk1,data/datanode/data/disk2";
diff --git 
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
 
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
index 0feb0098621..91b06a4f3f4 100644
--- 
a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
+++ 
b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensusServerImpl.java
@@ -130,6 +130,18 @@ public class IoTConsensusServerImpl {
   private final Condition stateMachineCondition = 
stateMachineLock.newCondition();
   private final String storageDir;
   private FolderManager recvFolderManager = null;
+
+  /**
+   * Per-snapshotId map of TsFile group key ({@code fileKey}) to the chosen 
receive folder. It keeps
+   * all companion files of one TsFile ({@code .tsfile}/{@code 
.tsfile.resource}/{@code
+   * .tsfile.mods2}/...) in the same receive folder, so the load phase can 
hard-link them inside a
+   * single data dir instead of falling back to a cross-disk copy. The {@code 
fileKey} rule matches
+   * {@code SnapshotLoader#createLinksFromSnapshotToSourceDir} so grouping is 
consistent end to end.
+   * Entries are removed once the snapshot is loaded or cleaned up.
+   */
+  private final ConcurrentHashMap<String, ConcurrentHashMap<String, String>>
+      snapshotReceiveFolderMap = new ConcurrentHashMap<>();
+
   private final TreeSet<Peer> configuration;
   private final AtomicLong searchIndex;
   private final LogDispatcher logDispatcher;
@@ -447,11 +459,32 @@ public class IoTConsensusServerImpl {
         return;
       }
 
-      recvFolderManager.getNextWithRetry(
-          folder -> {
-            writeSnapshotFragment(getSnapshotPath(folder, targetFilePath), 
fileChunk, fileOffset);
-            return null;
-          });
+      // Place every companion file of the same TsFile into one receive 
folder. The fileKey rule
+      // (filename before the first '.') matches SnapshotLoader so the group 
stays together. The
+      // folder is selected at most once per fileKey via computeIfAbsent, 
which is safe under the
+      // concurrent IoTConsensusRPC-Processor receivers.
+      String fileKey = getSnapshotFileKey(targetFilePath);
+      ConcurrentHashMap<String, String> folderMap =
+          snapshotReceiveFolderMap.computeIfAbsent(snapshotId, k -> new 
ConcurrentHashMap<>());
+      String folder;
+      try {
+        folder =
+            folderMap.computeIfAbsent(
+                fileKey,
+                k -> {
+                  try {
+                    return recvFolderManager.getNextFolder();
+                  } catch (DiskSpaceInsufficientException ex) {
+                    throw new RuntimeException(ex);
+                  }
+                });
+      } catch (RuntimeException re) {
+        if (re.getCause() instanceof DiskSpaceInsufficientException) {
+          throw (DiskSpaceInsufficientException) re.getCause();
+        }
+        throw re;
+      }
+      writeSnapshotFragment(getSnapshotPath(folder, targetFilePath), 
fileChunk, fileOffset);
     } catch (IOException e) {
       throw new ConsensusGroupModifyPeerException(
           String.format(IoTConsensusMessages.ERROR_RECEIVING_SNAPSHOT, 
snapshotId), e);
@@ -492,6 +525,14 @@ public class IoTConsensusServerImpl {
     return originalFilePath.substring(originalFilePath.indexOf(snapshotId));
   }
 
+  /**
+   * Groups companion files of one TsFile. Uses the same rule as {@code
+   * SnapshotLoader#createLinksFromSnapshotToSourceDir}: the file name up to 
the first {@code '.'}.
+   */
+  private String getSnapshotFileKey(String targetFilePath) {
+    return new File(targetFilePath).getName().split("\\.")[0];
+  }
+
   private void clearOldSnapshot() {
     File directory = new File(storageDir);
     File[] versionFiles = directory.listFiles((dir, name) -> 
name.startsWith(SNAPSHOT_DIR_NAME));
@@ -525,17 +566,22 @@ public class IoTConsensusServerImpl {
     // Note: an empty region produces a snapshot with zero fragments, so none 
of the receive folders
     // contains it. That is a legitimate (no-op) load, not a failure, so an 
absent snapshot must not
     // be reported as failure here.
-    List<File> snapshotDirs = new ArrayList<>();
-    for (String dir : recvFolderManager.getFolders()) {
-      File snapshotDir = getSnapshotPath(dir, snapshotId);
-      if (snapshotDir.exists()) {
-        snapshotDirs.add(snapshotDir);
+    try {
+      List<File> snapshotDirs = new ArrayList<>();
+      for (String dir : recvFolderManager.getFolders()) {
+        File snapshotDir = getSnapshotPath(dir, snapshotId);
+        if (snapshotDir.exists()) {
+          snapshotDirs.add(snapshotDir);
+        }
       }
+      if (snapshotDirs.isEmpty()) {
+        return true;
+      }
+      return stateMachine.loadSnapshot(snapshotDirs);
+    } finally {
+      // Receiving is finished for this snapshot; drop its receive-folder 
mapping.
+      snapshotReceiveFolderMap.remove(snapshotId);
     }
-    if (snapshotDirs.isEmpty()) {
-      return true;
-    }
-    return stateMachine.loadSnapshot(snapshotDirs);
   }
 
   private File getSnapshotPath(String curStorageDir, String 
snapshotRelativePath) {
@@ -1177,6 +1223,7 @@ public class IoTConsensusServerImpl {
   }
 
   public void cleanupSnapshot(String snapshotId) throws 
ConsensusGroupModifyPeerException {
+    snapshotReceiveFolderMap.remove(snapshotId);
     List<String> allDirs = new 
ArrayList<>(Collections.singletonList(storageDir));
     allDirs.addAll(recvFolderManager.getFolders());
     for (String dir : allDirs) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
index c20122bcf50..44ac962aaea 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBConfig.java
@@ -89,7 +89,9 @@ public class IoTDBConfig {
   private static final Logger logger = 
LoggerFactory.getLogger(IoTDBConfig.class);
   private static final String MULTI_DIR_STRATEGY_PREFIX = 
"org.apache.iotdb.commons.disk.strategy.";
   private static final String[] CLUSTER_ALLOWED_MULTI_DIR_STRATEGIES =
-      new String[] {"SequenceStrategy", "MaxDiskUsableSpaceFirstStrategy"};
+      new String[] {
+        "SequenceStrategy", "MaxDiskUsableSpaceFirstStrategy", 
"MinFolderOccupiedSpaceFirstStrategy"
+      };
   private static final String DEFAULT_MULTI_DIR_STRATEGY = "SequenceStrategy";
 
   private static final String STORAGE_GROUP_MATCHER = 
"([a-zA-Z0-9`_.\\-\\u2E80-\\u9FFF]+)";
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
index 59878f68e0d..7acda29f959 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/conf/IoTDBDescriptor.java
@@ -369,7 +369,8 @@ public class IoTDBDescriptor {
     try {
       conf.checkMultiDirStrategyClassName();
     } catch (Exception e) {
-      conf.setMultiDirStrategyClassName(oldMultiDirStrategyClassName.trim());
+      conf.setMultiDirStrategyClassName(
+          oldMultiDirStrategyClassName == null ? null : 
oldMultiDirStrategyClassName.trim());
       throw e;
     }
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/DataRegionConsensusImpl.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/DataRegionConsensusImpl.java
index a0735084fa5..22bab53a8d2 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/DataRegionConsensusImpl.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/consensus/DataRegionConsensusImpl.java
@@ -142,8 +142,9 @@ public class DataRegionConsensusImpl {
           .setThisNode(new TEndPoint(CONF.getInternalAddress(), 
CONF.getDataRegionConsensusPort()))
           .setStorageDir(CONF.getDataRegionConsensusDir())
           .setRecvSnapshotDirs(Arrays.asList(CONF.getLocalDataDirs()))
-          .setDirectoryStrategyType(
-              
DirectoryStrategyType.fromClassName(CONF.getMultiDirStrategyClassName()))
+          // IoTConsensus always balances received snapshot files by least 
occupied space,
+          // independent of the global dn_multi_dir_strategy.
+          
.setDirectoryStrategyType(DirectoryStrategyType.MIN_FOLDER_OCCUPIED_SPACE_FIRST_STRATEGY)
           .setConsensusGroupType(TConsensusGroupType.DataRegion)
           .setIoTConsensusConfig(
               IoTConsensusConfig.newBuilder()
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotLoader.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotLoader.java
index 588b0e21956..573d8fc293d 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotLoader.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/SnapshotLoader.java
@@ -153,7 +153,9 @@ public class SnapshotLoader {
       Map<String, String> fileTarget = new HashMap<>();
       for (String path : snapshotPaths) {
         File snapshotDir = new File(path);
-        createLinksFromSnapshotDirToDataDirWithoutLog(snapshotDir, fileTarget);
+        // IoTConsensus fragments arrive under different recv folders; do not 
map each
+        // fragment back to the same disk as its recv path, rely on fileTarget 
instead.
+        createLinksFromSnapshotDirToDataDirWithoutLog(snapshotDir, fileTarget, 
false);
         loadCompressionRatio(snapshotDir);
       }
       return loadSnapshot();
@@ -175,7 +177,7 @@ public class SnapshotLoader {
       }
       LOGGER.info(StorageEngineMessages.MOVING_SNAPSHOT_FILE_TO_DATA_DIRS);
       File snapshotDir = new File(snapshotPath);
-      createLinksFromSnapshotDirToDataDirWithoutLog(snapshotDir, new 
HashMap<>());
+      createLinksFromSnapshotDirToDataDirWithoutLog(snapshotDir, new 
HashMap<>(), true);
       loadCompressionRatio(snapshotDir);
       return loadSnapshot();
     } catch (IOException | DiskSpaceInsufficientException e) {
@@ -300,7 +302,7 @@ public class SnapshotLoader {
   }
 
   private void createLinksFromSnapshotDirToDataDirWithoutLog(
-      File sourceDir, Map<String, String> fileTarget)
+      File sourceDir, Map<String, String> fileTarget, boolean 
preferKeepSameDiskWhenLoading)
       throws IOException, DiskSpaceInsufficientException {
     if (!sourceDir.exists()) {
       throw new IOException(
@@ -346,7 +348,8 @@ public class SnapshotLoader {
                 + dataRegionId
                 + File.separator
                 + timePartitionFolder.getName();
-        createLinksFromSnapshotToSourceDir(targetSuffix, files, folderManager, 
fileTarget);
+        createLinksFromSnapshotToSourceDir(
+            targetSuffix, files, folderManager, fileTarget, 
preferKeepSameDiskWhenLoading);
       }
     }
 
@@ -365,7 +368,8 @@ public class SnapshotLoader {
                 + dataRegionId
                 + File.separator
                 + timePartitionFolder.getName();
-        createLinksFromSnapshotToSourceDir(targetSuffix, files, folderManager, 
fileTarget);
+        createLinksFromSnapshotToSourceDir(
+            targetSuffix, files, folderManager, fileTarget, 
preferKeepSameDiskWhenLoading);
       }
     }
   }
@@ -415,7 +419,8 @@ public class SnapshotLoader {
       String targetSuffix,
       File[] files,
       FolderManager folderManager,
-      Map<String, String> fileTarget)
+      Map<String, String> fileTarget,
+      boolean preferKeepSameDiskWhenLoading)
       throws IOException {
     for (File file : files) {
       checkTsFileResourceExists(file);
@@ -430,7 +435,8 @@ public class SnapshotLoader {
 
       try {
         String firstFolderOfSameDisk =
-            
IoTDBDescriptor.getInstance().getConfig().isKeepSameDiskWhenLoadingSnapshot()
+            preferKeepSameDiskWhenLoading
+                    && 
IoTDBDescriptor.getInstance().getConfig().isKeepSameDiskWhenLoadingSnapshot()
                 ? 
folderManager.getFirstFolderOfSameDisk(file.getAbsolutePath())
                 : null;
 
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/IoTDBSnapshotTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/IoTDBSnapshotTest.java
index ba7c9310945..2eedca1807f 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/IoTDBSnapshotTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/snapshot/IoTDBSnapshotTest.java
@@ -597,14 +597,15 @@ public class IoTDBSnapshotTest {
             String.class,
             File[].class,
             FolderManager.class,
-            Map.class);
+            Map.class,
+            boolean.class);
     method.setAccessible(true);
 
     SnapshotLoader loader = new SnapshotLoader("dummy", "root.testsg", "0");
 
     // Tracks fileKey -> chosen data dir, so files sharing a fileKey land in 
the same dir.
     Map<String, String> fileTarget = new HashMap<>();
-    method.invoke(loader, targetSuffix, files, folderManager, fileTarget);
+    method.invoke(loader, targetSuffix, files, folderManager, fileTarget, 
true);
 
     // The shared fileKey must be recorded exactly once, pointing at one of 
the data dirs.
     String fileKey = tsFile.getName().split("\\.")[0];

Reply via email to