From 35e36d58f587a2b8fa77b681804ee6891a51812b Mon Sep 17 00:00:00 2001 From: shuwenwei Date: Tue, 9 Jun 2026 16:01:39 +0800 Subject: [PATCH 1/4] Fix empty snapshot loading and region cleanup Allow loading snapshots that do not contain seq or unseq data directories so empty regions can be migrated successfully. Skip table disk usage index removal for tree-model regions and complete remove operations when no writer exists to avoid delete-region timeouts. --- .../storageengine/dataregion/DataRegion.java | 4 ++- .../dataregion/snapshot/SnapshotLoader.java | 10 +++--- .../TableDiskUsageIndex.java | 31 ++++++++++++------- .../snapshot/IoTDBSnapshotTest.java | 26 ++++++++++++++++ 4 files changed, 54 insertions(+), 17 deletions(-) diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/DataRegion.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/DataRegion.java index 7289e83cc9fc8..0860e7539a1c8 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/DataRegion.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/DataRegion.java @@ -2201,7 +2201,9 @@ public void deleteFolder(String systemDir) { databaseName + "-" + dataRegionIdString, systemDir); int regionId = dataRegionId.getId(); - TableDiskUsageIndex.getInstance().remove(databaseName, regionId); + if (isTableModel) { + TableDiskUsageIndex.getInstance().remove(databaseName, regionId); + } FileTimeIndexCacheRecorder.getInstance().removeFileTimeIndexCache(regionId); writeLock("deleteFolder"); try { 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 c32f2301289db..ba89bfc6c5432 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 @@ -250,6 +250,10 @@ private void deleteAllFilesInDataDirs() throws IOException { private void createLinksFromSnapshotDirToDataDirWithoutLog(File sourceDir) throws IOException, DiskSpaceInsufficientException { + if (!sourceDir.exists()) { + throw new IOException( + String.format("Cannot find snapshot directory %s", sourceDir.getAbsolutePath())); + } File seqFileDir = new File( sourceDir, @@ -267,10 +271,8 @@ private void createLinksFromSnapshotDirToDataDirWithoutLog(File sourceDir) + File.separator + dataRegionId); if (!seqFileDir.exists() && !unseqFileDir.exists()) { - throw new IOException( - String.format( - "Cannot find %s or %s", - seqFileDir.getAbsolutePath(), unseqFileDir.getAbsolutePath())); + LOGGER.warn("No seq or unseq files in snapshot {}, skip creating file links", sourceDir); + return; } FolderManager folderManager = new FolderManager( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/utils/tableDiskUsageIndex/TableDiskUsageIndex.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/utils/tableDiskUsageIndex/TableDiskUsageIndex.java index 40e04fc26789a..543356c98bc4d 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/utils/tableDiskUsageIndex/TableDiskUsageIndex.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/utils/tableDiskUsageIndex/TableDiskUsageIndex.java @@ -443,18 +443,25 @@ private RemoveRegionOperation(String database, int regionId) { @Override public void apply(TableDiskUsageIndex tableDiskUsageIndex) { - tableDiskUsageIndex.writerMap.computeIfPresent( - regionId, - (k, writer) -> { - if (writer.getActiveReaderNum() > 0) { - // If there are active readers, defer removal until all readers finish - writer.setRemovedFuture(future); - return writer; - } - writer.close(); - future.complete(null); - return null; - }); + DataRegionTableSizeIndexWriter removedWriter = null; + try { + removedWriter = + tableDiskUsageIndex.writerMap.computeIfPresent( + regionId, + (k, writer) -> { + if (writer.getActiveReaderNum() > 0) { + // If there are active readers, defer removal until all readers finish + writer.setRemovedFuture(future); + return writer; + } + writer.close(); + return null; + }); + } finally { + if (removedWriter == null) { + future.complete(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 f2ec78777cd99..29fe4f1358616 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 @@ -183,6 +183,32 @@ public void testCreateSnapshotWithUnclosedTsFile() } } + @Test + public void testLoadEmptySnapshotWithoutLog() throws IOException { + String[][] originDataDirs = IoTDBDescriptor.getInstance().getConfig().getTierDataDirs(); + IoTDBDescriptor.getInstance().getConfig().setTierDataDirs(testDataDirs); + TierManager.getInstance().resetFolders(); + File snapshotDir = new File("target" + File.separator + "empty-snapshot"); + try { + if (snapshotDir.exists()) { + FileUtils.recursivelyDeleteFolder(snapshotDir.getAbsolutePath()); + } + Assert.assertTrue(snapshotDir.mkdirs()); + + DataRegion dataRegion = + new SnapshotLoader(snapshotDir.getAbsolutePath(), testSgName, "0") + .loadSnapshotForStateMachine(); + + Assert.assertNotNull(dataRegion); + assertEquals(0, dataRegion.getTsFileManager().getTsFileList(true).size()); + assertEquals(0, dataRegion.getTsFileManager().getTsFileList(false).size()); + } finally { + FileUtils.recursivelyDeleteFolder(snapshotDir.getAbsolutePath()); + IoTDBDescriptor.getInstance().getConfig().setTierDataDirs(originDataDirs); + TierManager.getInstance().resetFolders(); + } + } + @Test public void testLoadSnapshot() throws IOException, WriteProcessException, DataRegionException, DirectoryNotLegalException { From ff9eeb24071f54267964bb482fa86ba143f4653a Mon Sep 17 00:00:00 2001 From: shuwenwei Date: Tue, 9 Jun 2026 16:26:31 +0800 Subject: [PATCH 2/4] fix i18n --- .../iotdb/db/i18n/StorageEngineMessages.java | 16 ++++++++ .../iotdb/db/i18n/StorageEngineMessages.java | 16 ++++++++ .../dataregion/snapshot/SnapshotLoader.java | 40 ++++++++++--------- 3 files changed, 54 insertions(+), 18 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 44a707a36c7eb..cb90fac0b068c 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 @@ -359,10 +359,26 @@ private StorageEngineMessages() {} // ======================== Snapshot ======================== public static final String EXCEPTION_LOAD_SNAPSHOT = "Exception occurs while load snapshot from {}"; + public static final String LOADING_SNAPSHOT_FOR = "Loading snapshot for {}-{}, source directory is {}"; + public static final String EXCEPTION_LOADING_SNAPSHOT_FOR = "Exception occurs when loading snapshot for {}-{}"; public static final String READING_SNAPSHOT_LOG_FILE = "Reading snapshot log file {}"; public static final String REMOVE_ALL_DATA_FILES_IN_ORIGINAL_DIR = "Remove all data files in original data dir"; public static final String FAILED_TO_REMOVE_ORIGIN_DATA_FILES = "Failed to remove origin data files"; public static final String MOVING_SNAPSHOT_FILE_TO_DATA_DIRS = "Moving snapshot file to data dirs"; + public static final String CANNOT_FIND_SNAPSHOT_DIRECTORY = "Cannot find snapshot directory %s"; + public static final String NO_SEQ_OR_UNSEQ_FILES_IN_SNAPSHOT = + "No seq or unseq files in snapshot {}, skip creating file links"; + public static final String EXCEPTION_DELETING_TIME_PARTITION_DIR = + "Exception occurs when deleting time partition directory for {}-{}"; + public static final String CANNOT_CREATE_LINK_FALLBACK_COPY = + "Cannot create link from {} to {}, fallback to copy"; + public static final String FAILED_TO_PROCESS_SNAPSHOT_FILE = + "Failed to process file {} in dir {}: {}"; + public static final String FAILED_TO_PROCESS_SNAPSHOT_FILE_AFTER_RETRIES = + "Failed to process file after retries. Source: %s, Target suffix: %s"; + public static final String SNAPSHOT_FILE_NUM_MISMATCH = + "The file num in log is %d, while file num in disk is %d"; + public static final String SNAPSHOT_FILE_NOT_IN_LOG = "File %s is not in the log file list"; public static final String NO_COMPRESSION_RATIO_FILE_IN_DIR = "No compression ratio file in dir {}"; public static final String CANNOT_LOAD_COMPRESSION_RATIO = "Cannot load compression ratio from {}"; public static final String LOADED_COMPRESSION_RATIO = "Loaded compression ratio from {}"; 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 69384eb33954e..e488c40067ea9 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 @@ -359,10 +359,26 @@ private StorageEngineMessages() {} // ======================== Snapshot ======================== public static final String EXCEPTION_LOAD_SNAPSHOT = "从 {} 加载快照时发生异常"; + public static final String LOADING_SNAPSHOT_FOR = "正在为 {}-{} 加载快照,源目录为 {}"; + public static final String EXCEPTION_LOADING_SNAPSHOT_FOR = "为 {}-{} 加载快照时发生异常"; public static final String READING_SNAPSHOT_LOG_FILE = "正在读取快照日志文件 {}"; public static final String REMOVE_ALL_DATA_FILES_IN_ORIGINAL_DIR = "移除原始数据目录中的所有数据文件"; public static final String FAILED_TO_REMOVE_ORIGIN_DATA_FILES = "移除原始数据文件失败"; public static final String MOVING_SNAPSHOT_FILE_TO_DATA_DIRS = "正在将快照文件移动到数据目录"; + public static final String CANNOT_FIND_SNAPSHOT_DIRECTORY = "找不到快照目录 %s"; + public static final String NO_SEQ_OR_UNSEQ_FILES_IN_SNAPSHOT = + "快照 {} 中没有顺序或乱序文件,跳过创建文件链接"; + public static final String EXCEPTION_DELETING_TIME_PARTITION_DIR = + "删除 {}-{} 的时间分区目录时发生异常"; + public static final String CANNOT_CREATE_LINK_FALLBACK_COPY = + "无法创建从 {} 到 {} 的链接,回退为复制"; + public static final String FAILED_TO_PROCESS_SNAPSHOT_FILE = + "处理文件 {} 失败,所在目录为 {}: {}"; + public static final String FAILED_TO_PROCESS_SNAPSHOT_FILE_AFTER_RETRIES = + "重试后仍无法处理文件。源文件: %s,目标后缀: %s"; + public static final String SNAPSHOT_FILE_NUM_MISMATCH = + "日志中的文件数为 %d,但磁盘中的文件数为 %d"; + public static final String SNAPSHOT_FILE_NOT_IN_LOG = "文件 %s 不在日志文件列表中"; public static final String NO_COMPRESSION_RATIO_FILE_IN_DIR = "目录 {} 中没有压缩率文件"; public static final String CANNOT_LOAD_COMPRESSION_RATIO = "无法从 {} 加载压缩率"; public static final String LOADED_COMPRESSION_RATIO = "已从 {} 加载压缩率"; 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 ba89bfc6c5432..d39d964df7be3 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 @@ -99,10 +99,7 @@ private File getSnapshotLogFile() { */ public DataRegion loadSnapshotForStateMachine() { LOGGER.info( - "Loading snapshot for {}-{}, source directory is {}", - storageGroupName, - dataRegionId, - snapshotPath); + StorageEngineMessages.LOADING_SNAPSHOT_FOR, storageGroupName, dataRegionId, snapshotPath); File snapshotLogFile = getSnapshotLogFile(); @@ -129,7 +126,7 @@ private DataRegion loadSnapshotWithoutLog() { return loadSnapshot(); } catch (IOException | DiskSpaceInsufficientException e) { LOGGER.error( - "Exception occurs when loading snapshot for {}-{}", storageGroupName, dataRegionId, e); + StorageEngineMessages.EXCEPTION_LOADING_SNAPSHOT_FOR, storageGroupName, dataRegionId, e); return null; } } @@ -240,7 +237,7 @@ private void deleteAllFilesInDataDirs() throws IOException { } } catch (IOException e) { LOGGER.error( - "Exception occurs when deleting time partition directory for {}-{}", + StorageEngineMessages.EXCEPTION_DELETING_TIME_PARTITION_DIR, storageGroupName, dataRegionId, e); @@ -252,7 +249,8 @@ private void createLinksFromSnapshotDirToDataDirWithoutLog(File sourceDir) throws IOException, DiskSpaceInsufficientException { if (!sourceDir.exists()) { throw new IOException( - String.format("Cannot find snapshot directory %s", sourceDir.getAbsolutePath())); + String.format( + StorageEngineMessages.CANNOT_FIND_SNAPSHOT_DIRECTORY, sourceDir.getAbsolutePath())); } File seqFileDir = new File( @@ -271,7 +269,7 @@ private void createLinksFromSnapshotDirToDataDirWithoutLog(File sourceDir) + File.separator + dataRegionId); if (!seqFileDir.exists() && !unseqFileDir.exists()) { - LOGGER.warn("No seq or unseq files in snapshot {}, skip creating file links", sourceDir); + LOGGER.warn(StorageEngineMessages.NO_SEQ_OR_UNSEQ_FILES_IN_SNAPSHOT, sourceDir); return; } FolderManager folderManager = @@ -331,16 +329,17 @@ private File createLinksFromSnapshotToSourceDir( if (!targetFile.getParentFile().exists() && !targetFile.getParentFile().mkdirs()) { throw new IOException( String.format( - "Cannot create directory %s", targetFile.getParentFile().getAbsolutePath())); + StorageEngineMessages.FAILED_TO_CREATE_DIR, + targetFile.getParentFile().getAbsolutePath())); } try { Files.createLink(targetFile.toPath(), file.toPath()); - LOGGER.debug("Created hard link from {} to {}", file, targetFile); + LOGGER.debug(StorageEngineMessages.CREATED_HARD_LINK, file, targetFile); fileTarget.put(fileKey, finalDir); return targetFile; } catch (IOException e) { - LOGGER.info("Cannot create link from {} to {}, fallback to copy", file, targetFile); + LOGGER.info(StorageEngineMessages.CANNOT_CREATE_LINK_FALLBACK_COPY, file, targetFile); } Files.copy(file.toPath(), targetFile.toPath()); @@ -348,7 +347,11 @@ private File createLinksFromSnapshotToSourceDir( return targetFile; } catch (Exception e) { LOGGER.warn( - "Failed to process file {} in dir {}: {}", file.getName(), finalDir, e.getMessage(), e); + StorageEngineMessages.FAILED_TO_PROCESS_SNAPSHOT_FILE, + file.getName(), + finalDir, + e.getMessage(), + e); throw e; } } @@ -383,8 +386,9 @@ private void createLinksFromSnapshotToSourceDir( } catch (Exception e) { throw new IOException( String.format( - "Failed to process file after retries. Source: %s, Target suffix: %s", - file.getAbsolutePath(), targetSuffix), + StorageEngineMessages.FAILED_TO_PROCESS_SNAPSHOT_FILE_AFTER_RETRIES, + file.getAbsolutePath(), + targetSuffix), e); } } @@ -411,8 +415,7 @@ private void createLinksFromSnapshotDirToDataDirWithLog() throws IOException { } if (fileCnt != loggedFileNum) { throw new IOException( - String.format( - "The file num in log is %d, while file num in disk is %d", loggedFileNum, fileCnt)); + String.format(StorageEngineMessages.SNAPSHOT_FILE_NUM_MISMATCH, loggedFileNum, fileCnt)); } } @@ -494,13 +497,14 @@ private void createLinksFromSourceToTarget(File targetDir, File[] files, Set Date: Tue, 9 Jun 2026 18:03:50 +0800 Subject: [PATCH 3/4] fix it --- .../org/apache/iotdb/db/it/IoTDBMiscIT.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBMiscIT.java b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBMiscIT.java index 809148535a587..46e02a5925911 100644 --- a/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBMiscIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBMiscIT.java @@ -55,46 +55,46 @@ public void testCompressionRatioFile() throws SQLException { Statement statement = connection.createStatement()) { statement.execute("insert into root.comprssion_ratio_file.d1(timestamp,s1) values(1,1.0)"); statement.execute("flush"); - // one global file and two data region file (including one AUDIT region) + // one global file and one data region file (including one AUDIT region) assertEquals(2, collectCompressionRatioFiles(nodeWrapper).size()); statement.execute("drop database root.comprssion_ratio_file"); - // one global file and system region file + // one global file // deleting a file may not be sensed by other processes instantly Awaitility.await() .atMost(10, TimeUnit.SECONDS) .pollDelay(100, TimeUnit.MILLISECONDS) - .until(() -> collectCompressionRatioFiles(nodeWrapper).size() == 2); + .until(() -> collectCompressionRatioFiles(nodeWrapper).size() == 1); statement.execute("insert into root.comprssion_ratio_file.d1(timestamp,s1) values(1,1.0)"); statement.execute("flush"); - assertEquals(3, collectCompressionRatioFiles(nodeWrapper).size()); + assertEquals(2, collectCompressionRatioFiles(nodeWrapper).size()); statement.execute("drop database root.comprssion_ratio_file"); Awaitility.await() .atMost(10, TimeUnit.SECONDS) .pollDelay(100, TimeUnit.MILLISECONDS) - .until(() -> collectCompressionRatioFiles(nodeWrapper).size() == 2); + .until(() -> collectCompressionRatioFiles(nodeWrapper).size() == 1); statement.execute("insert into root.comprssion_ratio_file.d1(timestamp,s1) values(1,1.0)"); statement.execute("flush"); - assertEquals(3, collectCompressionRatioFiles(nodeWrapper).size()); + assertEquals(2, collectCompressionRatioFiles(nodeWrapper).size()); statement.execute("insert into root.comprssion_ratio_file_2.d1(timestamp,s1) values(1,1.0)"); statement.execute("flush"); - assertEquals(4, collectCompressionRatioFiles(nodeWrapper).size()); + assertEquals(3, collectCompressionRatioFiles(nodeWrapper).size()); statement.execute("drop database root.comprssion_ratio_file"); Awaitility.await() .atMost(10, TimeUnit.SECONDS) .pollDelay(100, TimeUnit.MILLISECONDS) - .until(() -> collectCompressionRatioFiles(nodeWrapper).size() == 3); + .until(() -> collectCompressionRatioFiles(nodeWrapper).size() == 2); statement.execute("drop database root.comprssion_ratio_file_2"); Awaitility.await() .atMost(10, TimeUnit.SECONDS) .pollDelay(100, TimeUnit.MILLISECONDS) - .until(() -> collectCompressionRatioFiles(nodeWrapper).size() == 2); + .until(() -> collectCompressionRatioFiles(nodeWrapper).size() == 1); } finally { simpleEnv.cleanClusterEnvironment(); } From 4675517d6f6f0ec676ce4e389ba5f68a94fa7f5d Mon Sep 17 00:00:00 2001 From: shuwenwei Date: Tue, 9 Jun 2026 18:08:37 +0800 Subject: [PATCH 4/4] fix comment --- .../src/test/java/org/apache/iotdb/db/it/IoTDBMiscIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBMiscIT.java b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBMiscIT.java index 46e02a5925911..46ce8369eaa09 100644 --- a/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBMiscIT.java +++ b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBMiscIT.java @@ -55,7 +55,7 @@ public void testCompressionRatioFile() throws SQLException { Statement statement = connection.createStatement()) { statement.execute("insert into root.comprssion_ratio_file.d1(timestamp,s1) values(1,1.0)"); statement.execute("flush"); - // one global file and one data region file (including one AUDIT region) + // one global file and one data region file assertEquals(2, collectCompressionRatioFiles(nodeWrapper).size()); statement.execute("drop database root.comprssion_ratio_file");