This is an automated email from the ASF dual-hosted git repository. JackieTien97 pushed a commit to branch rc/2.0.10 in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit b0a27a46d150109508aefdc33fe826c15cba558b Author: Hongzhi Gao <[email protected]> AuthorDate: Wed Jun 24 14:55:26 2026 +0800 Fix/unconsensus tsfile mods (#18008) * Fix snapshot loader to keep tsfile and mods on the same data dir When IoTConsensus snapshot fragments land in different receive folders, share fileTarget across dirs so companion mods follow their tsfile. Add unit and cluster IT coverage for delete visibility after region migrate. * Revert unrelated occupied space cache IT config helpers These setters were only needed for an uncommitted manual IT and are not used by the snapshot loader fix. * Skip missing recv paths when selecting snapshot receive folder * Inline getOccupiedSpace call in MinFolderOccupiedSpaceFirstStrategy * Fix IoTDBSnapshotTest to match 4-arg createLinksFromSnapshotToSourceDir signature The test reflectively invoked the old 3-arg createLinksFromSnapshotToSourceDir(String, File[], FolderManager), but the production method now takes a Map<String,String> fileTarget as a 4th argument, causing NoSuchMethodException. Look up the current signature, pass a fileTarget map, and assert the shared fileKey is recorded exactly once at one of the data dirs. * Make IoTConsensus recv folder strategy follow dn_multi_dir_strategy and treat missing folder as empty Previously IoTConsensusServerImpl hardcoded MIN_FOLDER_OCCUPIED_SPACE_FIRST_STRATEGY for the snapshot receive folder. Thread the configured multi_dir_strategy through ConsensusConfig so the receiver follows dn_multi_dir_strategy (default SequenceStrategy). Add DirectoryStrategyType.fromClassName as the single class-name-to-type mapping and reuse it in TierManager. Also fix JVMCommonUtils.getOccupiedSpace to return 0 for a non-existent folder instead of letting Files.walk throw NoSuchFileException, which previously cascaded into a false DiskSpaceInsufficientException and flipped the node to read-only while receiving a snapshot into a not-yet-created directory. * Address review: warn on unrecognized multi-dir strategy and guard occupied-space race - DirectoryStrategyType.fromClassName now logs a warning when a non-null strategy name is unrecognized before falling back to SequenceStrategy. - JVMCommonUtils.getOccupiedSpace re-checks file existence inside mapToLong, since filter() and mapToLong() are not evaluated atomically and a file may be deleted in between. * Drop redundant comment in getOccupiedSpace * Use i18n UtilMessages for unrecognized multi-dir strategy warning --- .../it/env/cluster/config/MppDataNodeConfig.java | 6 + .../iotdb/it/env/cluster/node/DataNodeWrapper.java | 2 - .../it/env/remote/config/RemoteDataNodeConfig.java | 5 + .../apache/iotdb/itbase/env/DataNodeConfig.java | 2 + ...TDBRegionMigrateWithDeletionMultiDataDirIT.java | 180 +++++++++++++++++++++ .../iotdb/consensus/config/ConsensusConfig.java | 20 ++- .../apache/iotdb/consensus/iot/IoTConsensus.java | 5 + .../consensus/iot/IoTConsensusServerImpl.java | 5 +- .../db/consensus/DataRegionConsensusImpl.java | 3 + .../dataregion/snapshot/SnapshotLoader.java | 23 ++- .../db/storageengine/rescon/disk/TierManager.java | 18 +-- .../dataregion/snapshot/IoTDBSnapshotTest.java | 97 ++++++++++- .../apache/iotdb/commons/i18n/UtilMessages.java | 2 + .../apache/iotdb/commons/i18n/UtilMessages.java | 2 + .../disk/strategy/DirectoryStrategyType.java | 32 +++- .../MinFolderOccupiedSpaceFirstStrategy.java | 3 +- .../apache/iotdb/commons/utils/JVMCommonUtils.java | 11 +- .../disk/strategy/DirectoryStrategyTypeTest.java | 63 ++++++++ .../iotdb/commons/utils/JVMCommonUtilsTest.java | 27 ++++ 19 files changed, 471 insertions(+), 35 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 5e418072a7d..4caef4cc1f9 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 @@ -143,4 +143,10 @@ public class MppDataNodeConfig extends MppBaseConfig implements DataNodeConfig { setProperty("query_cost_stat_window", String.valueOf(queryCostStatWindow)); return this; } + + @Override + public DataNodeConfig setDnDataDirs(String dnDataDirs) { + setProperty("dn_data_dirs", dnDataDirs); + return this; + } } diff --git a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/node/DataNodeWrapper.java b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/node/DataNodeWrapper.java index dac6cf3fcc3..d205044bd52 100644 --- a/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/node/DataNodeWrapper.java +++ b/integration-test/src/main/java/org/apache/iotdb/it/env/cluster/node/DataNodeWrapper.java @@ -44,7 +44,6 @@ import static org.apache.iotdb.it.env.cluster.ClusterConstant.DEFAULT_DATA_NODE_ import static org.apache.iotdb.it.env.cluster.ClusterConstant.DEFAULT_DATA_NODE_PROPERTIES; import static org.apache.iotdb.it.env.cluster.ClusterConstant.DN_CONNECTION_TIMEOUT_MS; import static org.apache.iotdb.it.env.cluster.ClusterConstant.DN_CONSENSUS_DIR; -import static org.apache.iotdb.it.env.cluster.ClusterConstant.DN_DATA_DIRS; import static org.apache.iotdb.it.env.cluster.ClusterConstant.DN_DATA_REGION_CONSENSUS_PORT; import static org.apache.iotdb.it.env.cluster.ClusterConstant.DN_JOIN_CLUSTER_RETRY_INTERVAL_MS; import static org.apache.iotdb.it.env.cluster.ClusterConstant.DN_METRIC_INTERNAL_REPORTER_TYPE; @@ -125,7 +124,6 @@ public class DataNodeWrapper extends AbstractNodeWrapper { immutableNodeProperties.setProperty(IoTDBConstant.DN_SEED_CONFIG_NODE, seedConfigNode); immutableNodeProperties.setProperty(DN_SYSTEM_DIR, MppBaseConfig.NULL_VALUE); - immutableNodeProperties.setProperty(DN_DATA_DIRS, MppBaseConfig.NULL_VALUE); immutableNodeProperties.setProperty(DN_CONSENSUS_DIR, MppBaseConfig.NULL_VALUE); immutableNodeProperties.setProperty(DN_WAL_DIRS, MppBaseConfig.NULL_VALUE); immutableNodeProperties.setProperty(DN_TRACING_DIR, MppBaseConfig.NULL_VALUE); 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 bba4c964f95..aa73d962dba 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 @@ -98,4 +98,9 @@ public class RemoteDataNodeConfig implements DataNodeConfig { public DataNodeConfig setQueryCostStatWindow(int queryCostStatWindow) { return this; } + + @Override + public DataNodeConfig setDnDataDirs(String dnDataDirs) { + 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 d57015b1396..01a6114c206 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 @@ -53,4 +53,6 @@ public interface DataNodeConfig { DataNodeConfig setDataNodeMemoryProportion(String dataNodeMemoryProportion); DataNodeConfig setQueryCostStatWindow(int queryCostStatWindow); + + DataNodeConfig setDnDataDirs(String dnDataDirs); } 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 new file mode 100644 index 00000000000..43b2d1bc992 --- /dev/null +++ b/integration-test/src/test/java/org/apache/iotdb/confignode/it/regionmigration/pass/daily/iotv1/IoTDBRegionMigrateWithDeletionMultiDataDirIT.java @@ -0,0 +1,180 @@ +/* + * 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.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; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +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; + +@RunWith(IoTDBTestRunner.class) +@Category({ClusterIT.class}) +public class IoTDBRegionMigrateWithDeletionMultiDataDirIT { + + private static final String MULTI_DATA_DIRS = + "data/datanode/data/disk0,data/datanode/data/disk1,data/datanode/data/disk2"; + + @Before + public void setUp() throws Exception { + EnvFactory.getEnv() + .getConfig() + .getCommonConfig() + .setDataReplicationFactor(2) + .setDataRegionConsensusProtocolClass(ConsensusFactory.IOT_CONSENSUS); + EnvFactory.getEnv().getConfig().getDataNodeConfig().setDnDataDirs(MULTI_DATA_DIRS); + EnvFactory.getEnv().initClusterEnvironment(1, 3); + } + + @After + public void tearDown() throws Exception { + EnvFactory.getEnv().cleanClusterEnvironment(); + } + + @Test + public void testRegionMigratePreservesDeletionWithMultiDataDirs() throws Exception { + try (Connection connection = EnvFactory.getEnv().getTableConnection(); + 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("FLUSH"); + statement.execute("DELETE FROM t1 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); + + Pair<Integer, Set<Integer>> leaderAndNodes = dataRegionMapWithLeader.get(dataRegionIdForTest); + Set<Integer> allDataNodes = getAllDataNodes(statement); + int leaderId = leaderAndNodes.getLeft(); + int followerId = + leaderAndNodes.getRight().stream().filter(id -> id != leaderId).findFirst().orElseThrow(); + int destDataNodeId = + allDataNodes.stream() + .filter(id -> id != leaderId && id != followerId) + .findFirst() + .orElseThrow(); + + statement.execute( + String.format( + "migrate region %d from %d to %d", dataRegionIdForTest, leaderId, destDataNodeId)); + + final int finalDestDataNodeId = destDataNodeId; + Awaitility.await() + .atMost(10, TimeUnit.MINUTES) + .pollDelay(1, TimeUnit.SECONDS) + .pollInterval(2, TimeUnit.SECONDS) + .untilAsserted( + () -> { + try (ResultSet showRegions = statement.executeQuery("SHOW REGIONS")) { + boolean migrated = false; + while (showRegions.next()) { + if (showRegions.getInt("RegionId") == dataRegionIdForTest + && showRegions.getInt("DataNodeId") == finalDestDataNodeId) { + migrated = true; + break; + } + } + Assert.assertTrue(migrated); + } + }); + + assertDeletionVisibleOnAllReplicas(statement, dataRegionIdForTest, 1); + } + } + + private void assertDeletionVisibleOnAllReplicas( + Statement statement, int dataRegionId, int expectedCount) throws Exception { + Set<Integer> replicaDataNodeIds = getReplicaDataNodeIds(statement, dataRegionId); + for (int dataNodeId : replicaDataNodeIds) { + DataNodeWrapper dataNodeWrapper = + EnvFactory.getEnv().dataNodeIdToWrapper(dataNodeId).orElseThrow(); + Awaitility.await() + .atMost(2, TimeUnit.MINUTES) + .pollDelay(500, TimeUnit.MILLISECONDS) + .pollInterval(1, TimeUnit.SECONDS) + .untilAsserted(() -> assertDeletionVisibleOnReplica(dataNodeWrapper, expectedCount)); + } + } + + 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); + Statement dataNodeStatement = connection.createStatement()) { + dataNodeStatement.execute("USE test"); + try (ResultSet countResultSet = dataNodeStatement.executeQuery("SELECT COUNT(s1) FROM t1")) { + Assert.assertTrue(countResultSet.next()); + Assert.assertEquals(expectedCount, countResultSet.getLong(1)); + } + try (ResultSet deletedRangeResultSet = + dataNodeStatement.executeQuery("SELECT s1 FROM t1 WHERE time <= 200")) { + Assert.assertFalse(deletedRangeResultSet.next()); + } + } + } + + private Set<Integer> getReplicaDataNodeIds(Statement statement, int dataRegionId) + throws Exception { + Set<Integer> replicaDataNodeIds = new HashSet<>(); + try (ResultSet showRegions = statement.executeQuery("SHOW REGIONS")) { + while (showRegions.next()) { + if ("DataRegion".equals(showRegions.getString("Type")) + && showRegions.getInt("RegionId") == dataRegionId) { + replicaDataNodeIds.add(showRegions.getInt("DataNodeId")); + } + } + } + Assert.assertFalse(replicaDataNodeIds.isEmpty()); + return replicaDataNodeIds; + } +} diff --git a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/config/ConsensusConfig.java b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/config/ConsensusConfig.java index f834e41a4c9..d54299992fe 100644 --- a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/config/ConsensusConfig.java +++ b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/config/ConsensusConfig.java @@ -21,6 +21,7 @@ package org.apache.iotdb.consensus.config; import org.apache.iotdb.common.rpc.thrift.TConsensusGroupType; import org.apache.iotdb.common.rpc.thrift.TEndPoint; +import org.apache.iotdb.commons.disk.strategy.DirectoryStrategyType; import java.util.List; import java.util.Optional; @@ -35,6 +36,7 @@ public class ConsensusConfig { private final RatisConfig ratisConfig; private final IoTConsensusConfig iotConsensusConfig; private final IoTConsensusV2Config iotConsensusV2Config; + private final DirectoryStrategyType directoryStrategyType; private ConsensusConfig( TEndPoint thisNode, @@ -44,7 +46,8 @@ public class ConsensusConfig { TConsensusGroupType consensusGroupType, RatisConfig ratisConfig, IoTConsensusConfig iotConsensusConfig, - IoTConsensusV2Config iotConsensusV2Config) { + IoTConsensusV2Config iotConsensusV2Config, + DirectoryStrategyType directoryStrategyType) { this.thisNodeEndPoint = thisNode; this.thisNodeId = thisNodeId; this.storageDir = storageDir; @@ -53,6 +56,7 @@ public class ConsensusConfig { this.ratisConfig = ratisConfig; this.iotConsensusConfig = iotConsensusConfig; this.iotConsensusV2Config = iotConsensusV2Config; + this.directoryStrategyType = directoryStrategyType; } public TEndPoint getThisNodeEndPoint() { @@ -87,6 +91,10 @@ public class ConsensusConfig { return iotConsensusV2Config; } + public DirectoryStrategyType getDirectoryStrategyType() { + return directoryStrategyType; + } + public static ConsensusConfig.Builder newBuilder() { return new ConsensusConfig.Builder(); } @@ -101,6 +109,8 @@ public class ConsensusConfig { private RatisConfig ratisConfig; private IoTConsensusConfig iotConsensusConfig; private IoTConsensusV2Config iotConsensusV2Config; + private DirectoryStrategyType directoryStrategyType = + DirectoryStrategyType.MIN_FOLDER_OCCUPIED_SPACE_FIRST_STRATEGY; public ConsensusConfig build() { return new ConsensusConfig( @@ -113,7 +123,8 @@ public class ConsensusConfig { Optional.ofNullable(iotConsensusConfig) .orElseGet(() -> IoTConsensusConfig.newBuilder().build()), Optional.ofNullable(iotConsensusV2Config) - .orElseGet(() -> IoTConsensusV2Config.newBuilder().build())); + .orElseGet(() -> IoTConsensusV2Config.newBuilder().build()), + directoryStrategyType); } public Builder setThisNode(TEndPoint thisNode) { @@ -155,5 +166,10 @@ public class ConsensusConfig { this.iotConsensusV2Config = iotConsensusV2Config; return this; } + + public Builder setDirectoryStrategyType(DirectoryStrategyType directoryStrategyType) { + this.directoryStrategyType = directoryStrategyType; + return this; + } } } diff --git a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensus.java b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensus.java index d84f0a5aa81..6dd7cfcb4dc 100644 --- a/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensus.java +++ b/iotdb-core/consensus/src/main/java/org/apache/iotdb/consensus/iot/IoTConsensus.java @@ -26,6 +26,7 @@ import org.apache.iotdb.commons.concurrent.IoTDBThreadPoolFactory; import org.apache.iotdb.commons.concurrent.ThreadName; import org.apache.iotdb.commons.concurrent.threadpool.ScheduledExecutorUtil; import org.apache.iotdb.commons.consensus.ConsensusGroupId; +import org.apache.iotdb.commons.disk.strategy.DirectoryStrategyType; import org.apache.iotdb.commons.exception.DiskSpaceInsufficientException; import org.apache.iotdb.commons.exception.StartupException; import org.apache.iotdb.commons.request.IConsensusRequest; @@ -96,6 +97,7 @@ public class IoTConsensus implements IConsensus { private final int thisNodeId; private final File storageDir; private final List<String> recvSnapshotDirs; + private final DirectoryStrategyType recvFolderStrategyType; private final IStateMachine.Registry registry; private final Map<ConsensusGroupId, IoTConsensusServerImpl> stateMachineMap = new ConcurrentHashMap<>(); @@ -113,6 +115,7 @@ public class IoTConsensus implements IConsensus { this.thisNodeId = config.getThisNodeId(); this.storageDir = new File(config.getStorageDir()); this.recvSnapshotDirs = config.getRecvSnapshotDirs(); + this.recvFolderStrategyType = config.getDirectoryStrategyType(); this.config = config.getIotConsensusConfig(); this.registry = registry; this.service = new IoTConsensusRPCService(thisNode, config.getIotConsensusConfig()); @@ -181,6 +184,7 @@ public class IoTConsensus implements IConsensus { new IoTConsensusServerImpl( path.toString(), recvSnapshotDirs, + recvFolderStrategyType, new Peer(consensusGroupId, thisNodeId, thisNode), new TreeSet<>(), registry.apply(consensusGroupId), @@ -295,6 +299,7 @@ public class IoTConsensus implements IConsensus { new IoTConsensusServerImpl( path, recvSnapshotDirs, + recvFolderStrategyType, new Peer(groupId, thisNodeId, thisNode), new TreeSet<>(peers), registry.apply(groupId), 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 651391eab01..285dbfe3255 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 @@ -137,6 +137,7 @@ public class IoTConsensusServerImpl { public IoTConsensusServerImpl( String storageDir, List<String> recvSnapshotDirs, + DirectoryStrategyType recvFolderStrategyType, Peer thisNode, TreeSet<Peer> configuration, IStateMachine stateMachine, @@ -156,9 +157,7 @@ public class IoTConsensusServerImpl { snapshotDirs.add(storageDir); } - this.recvFolderManager = - new FolderManager( - snapshotDirs, DirectoryStrategyType.MIN_FOLDER_OCCUPIED_SPACE_FIRST_STRATEGY); + this.recvFolderManager = new FolderManager(snapshotDirs, recvFolderStrategyType); this.thisNode = thisNode; this.stateMachine = stateMachine; this.cacheQueueMap = new ConcurrentHashMap<>(); 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 e2ddd74bbe9..d8613c8f8de 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 @@ -25,6 +25,7 @@ import org.apache.iotdb.commons.conf.CommonConfig; import org.apache.iotdb.commons.conf.CommonDescriptor; import org.apache.iotdb.commons.consensus.ConsensusGroupId; import org.apache.iotdb.commons.consensus.DataRegionId; +import org.apache.iotdb.commons.disk.strategy.DirectoryStrategyType; import org.apache.iotdb.commons.memory.IMemoryBlock; import org.apache.iotdb.commons.memory.MemoryBlockType; import org.apache.iotdb.commons.pipe.agent.plugin.builtin.BuiltinPipePlugin; @@ -141,6 +142,8 @@ public class DataRegionConsensusImpl { .setThisNode(new TEndPoint(CONF.getInternalAddress(), CONF.getDataRegionConsensusPort())) .setStorageDir(CONF.getDataRegionConsensusDir()) .setRecvSnapshotDirs(Arrays.asList(CONF.getLocalDataDirs())) + .setDirectoryStrategyType( + DirectoryStrategyType.fromClassName(CONF.getMultiDirStrategyClassName())) .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 33f87187c91..e17930f66d7 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 @@ -144,9 +144,14 @@ public class SnapshotLoader { try { deleteAllFilesInDataDirs(); LOGGER.info(StorageEngineMessages.REMOVE_ALL_DATA_FILES_IN_ORIGINAL_DIR); + // IoTConsensus may spread the fragments of one snapshot across several receive folders. + // The fileTarget map must be shared across all of them so that a tsfile and its companion + // files (resource, exclusive mods, etc.) are relinked to the same data dir even when their + // fragments were received on different disks. + Map<String, String> fileTarget = new HashMap<>(); for (String path : snapshotPaths) { File snapshotDir = new File(path); - createLinksFromSnapshotDirToDataDirWithoutLog(snapshotDir); + createLinksFromSnapshotDirToDataDirWithoutLog(snapshotDir, fileTarget); loadCompressionRatio(snapshotDir); } return loadSnapshot(); @@ -168,7 +173,7 @@ public class SnapshotLoader { } LOGGER.info(StorageEngineMessages.MOVING_SNAPSHOT_FILE_TO_DATA_DIRS); File snapshotDir = new File(snapshotPath); - createLinksFromSnapshotDirToDataDirWithoutLog(snapshotDir); + createLinksFromSnapshotDirToDataDirWithoutLog(snapshotDir, new HashMap<>()); loadCompressionRatio(snapshotDir); return loadSnapshot(); } catch (IOException | DiskSpaceInsufficientException e) { @@ -292,7 +297,8 @@ public class SnapshotLoader { } } - private void createLinksFromSnapshotDirToDataDirWithoutLog(File sourceDir) + private void createLinksFromSnapshotDirToDataDirWithoutLog( + File sourceDir, Map<String, String> fileTarget) throws IOException, DiskSpaceInsufficientException { if (!sourceDir.exists()) { throw new IOException( @@ -338,7 +344,7 @@ public class SnapshotLoader { + dataRegionId + File.separator + timePartitionFolder.getName(); - createLinksFromSnapshotToSourceDir(targetSuffix, files, folderManager); + createLinksFromSnapshotToSourceDir(targetSuffix, files, folderManager, fileTarget); } } @@ -357,7 +363,7 @@ public class SnapshotLoader { + dataRegionId + File.separator + timePartitionFolder.getName(); - createLinksFromSnapshotToSourceDir(targetSuffix, files, folderManager); + createLinksFromSnapshotToSourceDir(targetSuffix, files, folderManager, fileTarget); } } } @@ -404,8 +410,11 @@ public class SnapshotLoader { } private void createLinksFromSnapshotToSourceDir( - String targetSuffix, File[] files, FolderManager folderManager) throws IOException { - Map<String, String> fileTarget = new HashMap<>(); + String targetSuffix, + File[] files, + FolderManager folderManager, + Map<String, String> fileTarget) + throws IOException { for (File file : files) { String fileKey = file.getName().split("\\.")[0]; String dataDir = fileTarget.get(fileKey); diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/rescon/disk/TierManager.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/rescon/disk/TierManager.java index 200be139664..561da825e9c 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/rescon/disk/TierManager.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/rescon/disk/TierManager.java @@ -22,9 +22,6 @@ import org.apache.iotdb.commons.conf.CommonDescriptor; import org.apache.iotdb.commons.conf.IoTDBConstant; import org.apache.iotdb.commons.disk.FolderManager; import org.apache.iotdb.commons.disk.strategy.DirectoryStrategyType; -import org.apache.iotdb.commons.disk.strategy.MaxDiskUsableSpaceFirstStrategy; -import org.apache.iotdb.commons.disk.strategy.MinFolderOccupiedSpaceFirstStrategy; -import org.apache.iotdb.commons.disk.strategy.RandomOnDiskUsableSpaceStrategy; import org.apache.iotdb.commons.exception.DiskSpaceInsufficientException; import org.apache.iotdb.db.conf.IoTDBConfig; import org.apache.iotdb.db.conf.IoTDBDescriptor; @@ -98,19 +95,8 @@ public class TierManager { } public synchronized void initFolders() { - try { - String strategyName = Class.forName(config.getMultiDirStrategyClassName()).getSimpleName(); - if (strategyName.equals(MaxDiskUsableSpaceFirstStrategy.class.getSimpleName())) { - directoryStrategyType = DirectoryStrategyType.MAX_DISK_USABLE_SPACE_FIRST_STRATEGY; - } else if (strategyName.equals(MinFolderOccupiedSpaceFirstStrategy.class.getSimpleName())) { - directoryStrategyType = DirectoryStrategyType.MIN_FOLDER_OCCUPIED_SPACE_FIRST_STRATEGY; - } else if (strategyName.equals(RandomOnDiskUsableSpaceStrategy.class.getSimpleName())) { - directoryStrategyType = DirectoryStrategyType.RANDOM_ON_DISK_USABLE_SPACE_STRATEGY; - } - } catch (Exception e) { - logger.error( - "Can't find strategy {} for mult-directories.", config.getMultiDirStrategyClassName(), e); - } + directoryStrategyType = + DirectoryStrategyType.fromClassName(config.getMultiDirStrategyClassName()); config.updatePath(); String[][] tierDirs = config.getTierDataDirs(); 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 d042d2a2ae8..ba7c9310945 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 @@ -52,7 +52,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.apache.iotdb.consensus.iot.IoTConsensusServerImpl.SNAPSHOT_DIR_NAME; import static org.apache.tsfile.common.constant.TsFileConstant.PATH_SEPARATOR; @@ -258,6 +260,51 @@ public class IoTDBSnapshotTest { loadSnapshotSpreadAcrossReceiveFolders(false); } + /** + * When IoTConsensus spreads a tsfile and its exclusive mods across different receive folders, the + * loader must still relink them to the same data dir. Otherwise the mods file is not found next + * to the tsfile and deletion markers are silently ignored. + */ + @Test + public void testLoadSnapshotKeepsTsFileAndModsOnSameDataDirWhenFragmentsAreSpread() + throws IOException, WriteProcessException { + String[][] originDataDirs = IoTDBDescriptor.getInstance().getConfig().getTierDataDirs(); + IoTDBDescriptor.getInstance().getConfig().setTierDataDirs(testDataDirs); + TierManager.getInstance().resetFolders(); + String recvBase0 = "target" + File.separator + "recv-snapshot-mods-0"; + String recvBase1 = "target" + File.separator + "recv-snapshot-mods-1"; + File recvFolder0 = new File(recvBase0, SNAPSHOT_DIR_NAME); + File recvFolder1 = new File(recvBase1, SNAPSHOT_DIR_NAME); + try { + Assert.assertTrue(recvFolder0.mkdirs()); + Assert.assertTrue(recvFolder1.mkdirs()); + + writeSnapshotFragmentWithExclusiveModsSpread(recvFolder0.getAbsolutePath(), 0, recvFolder1); + + DataRegion dataRegion = + new SnapshotLoader( + Arrays.asList(recvFolder0.getAbsolutePath(), recvFolder1.getAbsolutePath()), + testSgName, + "0") + .loadSnapshotForStateMachine(); + + Assert.assertNotNull(dataRegion); + TsFileResource resource = dataRegion.getTsFileManager().getTsFileList(true).get(0); + File tsFile = resource.getTsFile(); + File modsFile = + org.apache.iotdb.db.storageengine.dataregion.modification.ModificationFile + .getExclusiveMods(tsFile); + Assert.assertTrue(modsFile.exists()); + Assert.assertEquals( + tsFile.getParentFile().getAbsolutePath(), modsFile.getParentFile().getAbsolutePath()); + } finally { + FileUtils.recursivelyDeleteFolder(recvBase0); + FileUtils.recursivelyDeleteFolder(recvBase1); + IoTDBDescriptor.getInstance().getConfig().setTierDataDirs(originDataDirs); + TierManager.getInstance().resetFolders(); + } + } + /** * The fragments of one snapshot are disjoint across the receive folders, so the order in which * the folders are relinked must not change the loaded data. This loads the same spread snapshot @@ -343,6 +390,41 @@ public class IoTDBSnapshotTest { resource.serialize(); } + private void writeSnapshotFragmentWithExclusiveModsSpread( + String tsFileRecvSnapshotDir, int i, File modsRecvFolder) + throws IOException, WriteProcessException { + writeSnapshotFragment(tsFileRecvSnapshotDir, i); + String tsFileName = String.format("%d-%d-0-0.tsfile", i + 1, i + 1); + File tsFile = + new File( + tsFileRecvSnapshotDir + + File.separator + + "sequence" + + File.separator + + testSgName + + File.separator + + "0" + + File.separator + + "0" + + File.separator + + tsFileName); + File sourceMods = + org.apache.iotdb.db.storageengine.dataregion.modification.ModificationFile.getExclusiveMods( + tsFile); + Assert.assertTrue(sourceMods.exists() || sourceMods.createNewFile()); + + File targetModsDir = + new File( + modsRecvFolder, + "sequence" + File.separator + testSgName + File.separator + "0" + File.separator + "0"); + Assert.assertTrue(targetModsDir.exists() || targetModsDir.mkdirs()); + Files.copy( + sourceMods.toPath(), + new File(targetModsDir, sourceMods.getName()).toPath(), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + Files.delete(sourceMods.toPath()); + } + @Ignore("Need manual execution to specify different disks") @Test public void testLoadSnapshotNoHardLink() @@ -511,12 +593,23 @@ public class IoTDBSnapshotTest { Method method = SnapshotLoader.class.getDeclaredMethod( - "createLinksFromSnapshotToSourceDir", String.class, File[].class, FolderManager.class); + "createLinksFromSnapshotToSourceDir", + String.class, + File[].class, + FolderManager.class, + Map.class); method.setAccessible(true); SnapshotLoader loader = new SnapshotLoader("dummy", "root.testsg", "0"); - method.invoke(loader, targetSuffix, files, folderManager); + // 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); + + // The shared fileKey must be recorded exactly once, pointing at one of the data dirs. + String fileKey = tsFile.getName().split("\\.")[0]; + Assert.assertEquals(1, fileTarget.size()); + Assert.assertTrue(Arrays.asList(dataDirs).contains(fileTarget.get(fileKey))); // verify: only ONE dir contains all three files int hitDirCount = 0; diff --git a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/UtilMessages.java b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/UtilMessages.java index b13ad217f7a..395754dfb77 100644 --- a/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/UtilMessages.java +++ b/iotdb-core/node-commons/src/main/i18n/en/org/apache/iotdb/commons/i18n/UtilMessages.java @@ -119,6 +119,8 @@ public final class UtilMessages { "Disk space is insufficient, change system mode to read-only."; public static final String CANNOT_CALCULATE_OCCUPIED_SPACE = "Cannot calculate occupied space of folder {}"; + public static final String UNRECOGNIZED_MULTI_DIR_STRATEGY = + "Unrecognized multi-dir strategy '{}', falling back to {}."; // ======================== NodeUrlUtils ======================== diff --git a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/UtilMessages.java b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/UtilMessages.java index f4215f6e449..1a1b97e7f95 100644 --- a/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/UtilMessages.java +++ b/iotdb-core/node-commons/src/main/i18n/zh/org/apache/iotdb/commons/i18n/UtilMessages.java @@ -117,6 +117,8 @@ public final class UtilMessages { "磁盘空间不足,系统切换为只读模式。"; public static final String CANNOT_CALCULATE_OCCUPIED_SPACE = "无法计算文件夹 {} 的已占用空间"; + public static final String UNRECOGNIZED_MULTI_DIR_STRATEGY = + "无法识别的多目录策略 '{}',回退为 {}。"; // ======================== NodeUrlUtils ======================== diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/DirectoryStrategyType.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/DirectoryStrategyType.java index 2d081dd87f5..853edf899cf 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/DirectoryStrategyType.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/DirectoryStrategyType.java @@ -18,9 +18,39 @@ */ package org.apache.iotdb.commons.disk.strategy; +import org.apache.iotdb.commons.i18n.UtilMessages; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public enum DirectoryStrategyType { SEQUENCE_STRATEGY, MAX_DISK_USABLE_SPACE_FIRST_STRATEGY, MIN_FOLDER_OCCUPIED_SPACE_FIRST_STRATEGY, - RANDOM_ON_DISK_USABLE_SPACE_STRATEGY, + RANDOM_ON_DISK_USABLE_SPACE_STRATEGY; + + private static final Logger LOGGER = LoggerFactory.getLogger(DirectoryStrategyType.class); + + /** + * Resolves the strategy type from a multi-dir strategy class name as configured by {@code + * dn_multi_dir_strategy}. Accepts either a simple class name (e.g. {@code SequenceStrategy}) or a + * fully-qualified one. Returns {@link #SEQUENCE_STRATEGY} for a null or unrecognized value, which + * matches the configured default. + */ + public static DirectoryStrategyType fromClassName(String className) { + if (className != null) { + String simpleName = className.substring(className.lastIndexOf('.') + 1); + if (simpleName.equals(MaxDiskUsableSpaceFirstStrategy.class.getSimpleName())) { + return MAX_DISK_USABLE_SPACE_FIRST_STRATEGY; + } else if (simpleName.equals(MinFolderOccupiedSpaceFirstStrategy.class.getSimpleName())) { + return MIN_FOLDER_OCCUPIED_SPACE_FIRST_STRATEGY; + } else if (simpleName.equals(RandomOnDiskUsableSpaceStrategy.class.getSimpleName())) { + return RANDOM_ON_DISK_USABLE_SPACE_STRATEGY; + } else if (simpleName.equals(SequenceStrategy.class.getSimpleName())) { + return SEQUENCE_STRATEGY; + } + LOGGER.warn(UtilMessages.UNRECOGNIZED_MULTI_DIR_STRATEGY, className, SEQUENCE_STRATEGY); + } + return SEQUENCE_STRATEGY; + } } diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/MinFolderOccupiedSpaceFirstStrategy.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/MinFolderOccupiedSpaceFirstStrategy.java index 285b8b1d5b0..6a893d47aa1 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/MinFolderOccupiedSpaceFirstStrategy.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/disk/strategy/MinFolderOccupiedSpaceFirstStrategy.java @@ -25,6 +25,7 @@ import org.apache.iotdb.commons.utils.JVMCommonUtils; import org.apache.iotdb.commons.utils.TestOnly; import java.io.IOException; +import java.io.UncheckedIOException; /** * Selects the folder with the least occupied space. @@ -118,7 +119,7 @@ public class MinFolderOccupiedSpaceFirstStrategy extends DirectoryStrategy { } try { cachedOccupiedSpace[i] = JVMCommonUtils.getOccupiedSpace(folder); - } catch (IOException e) { + } catch (IOException | UncheckedIOException e) { LOGGER.error(UtilMessages.CANNOT_CALCULATE_OCCUPIED_SPACE, folder, e); cachedOccupiedSpace[i] = Long.MAX_VALUE; } diff --git a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/utils/JVMCommonUtils.java b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/utils/JVMCommonUtils.java index ccbe525d3d5..f041c46c9a3 100644 --- a/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/utils/JVMCommonUtils.java +++ b/iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/utils/JVMCommonUtils.java @@ -111,8 +111,17 @@ public class JVMCommonUtils { public static long getOccupiedSpace(String folderPath) throws IOException { Path folder = Paths.get(folderPath); + if (!Files.exists(folder)) { + return 0; + } try (Stream<Path> s = Files.walk(folder)) { - return s.filter(p -> p.toFile().isFile()).mapToLong(p -> p.toFile().length()).sum(); + return s.filter(p -> p.toFile().isFile()) + .mapToLong( + p -> { + File file = p.toFile(); + return file.exists() ? file.length() : 0L; + }) + .sum(); } } diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/disk/strategy/DirectoryStrategyTypeTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/disk/strategy/DirectoryStrategyTypeTest.java new file mode 100644 index 00000000000..90e56cf8f0b --- /dev/null +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/disk/strategy/DirectoryStrategyTypeTest.java @@ -0,0 +1,63 @@ +/* + * 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.commons.disk.strategy; + +import org.junit.Assert; +import org.junit.Test; + +public class DirectoryStrategyTypeTest { + + @Test + public void fromSimpleClassName() { + Assert.assertEquals( + DirectoryStrategyType.SEQUENCE_STRATEGY, + DirectoryStrategyType.fromClassName("SequenceStrategy")); + Assert.assertEquals( + DirectoryStrategyType.MAX_DISK_USABLE_SPACE_FIRST_STRATEGY, + DirectoryStrategyType.fromClassName("MaxDiskUsableSpaceFirstStrategy")); + Assert.assertEquals( + DirectoryStrategyType.MIN_FOLDER_OCCUPIED_SPACE_FIRST_STRATEGY, + DirectoryStrategyType.fromClassName("MinFolderOccupiedSpaceFirstStrategy")); + Assert.assertEquals( + DirectoryStrategyType.RANDOM_ON_DISK_USABLE_SPACE_STRATEGY, + DirectoryStrategyType.fromClassName("RandomOnDiskUsableSpaceStrategy")); + } + + @Test + public void fromFullyQualifiedClassName() { + Assert.assertEquals( + DirectoryStrategyType.SEQUENCE_STRATEGY, + DirectoryStrategyType.fromClassName(SequenceStrategy.class.getName())); + Assert.assertEquals( + DirectoryStrategyType.MIN_FOLDER_OCCUPIED_SPACE_FIRST_STRATEGY, + DirectoryStrategyType.fromClassName(MinFolderOccupiedSpaceFirstStrategy.class.getName())); + } + + @Test + public void nullOrUnknownFallsBackToSequence() { + // The configured default (dn_multi_dir_strategy=SequenceStrategy) and any unrecognized value + // must resolve to SEQUENCE_STRATEGY. + Assert.assertEquals( + DirectoryStrategyType.SEQUENCE_STRATEGY, DirectoryStrategyType.fromClassName(null)); + Assert.assertEquals( + DirectoryStrategyType.SEQUENCE_STRATEGY, + DirectoryStrategyType.fromClassName("NoSuchStrategy")); + } +} diff --git a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/utils/JVMCommonUtilsTest.java b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/utils/JVMCommonUtilsTest.java index 4a247cfb765..d35d3a52ea2 100644 --- a/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/utils/JVMCommonUtilsTest.java +++ b/iotdb-core/node-commons/src/test/java/org/apache/iotdb/commons/utils/JVMCommonUtilsTest.java @@ -20,10 +20,19 @@ package org.apache.iotdb.commons.utils; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; public class JVMCommonUtilsTest { + @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); + @Test public void getJdkVersionTest() { try { @@ -39,4 +48,22 @@ public class JVMCommonUtilsTest { Assert.fail(); } } + + @Test + public void getOccupiedSpaceMissingFolderReturnsZero() throws IOException { + File missing = new File(tempFolder.getRoot(), "does-not-exist"); + Assert.assertFalse(missing.exists()); + // A non-existent folder must be treated as empty rather than throwing NoSuchFileException. + Assert.assertEquals(0L, JVMCommonUtils.getOccupiedSpace(missing.getAbsolutePath())); + } + + @Test + public void getOccupiedSpaceSumsFileSizes() throws IOException { + File dir = tempFolder.newFolder("data"); + byte[] payload = "hello-iotdb".getBytes(StandardCharsets.UTF_8); + Files.write(new File(dir, "a.txt").toPath(), payload); + Files.write(new File(dir, "b.txt").toPath(), payload); + Assert.assertEquals( + 2L * payload.length, JVMCommonUtils.getOccupiedSpace(dir.getAbsolutePath())); + } }
