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 ea0b0099227 Load: Fix active load reliability issues (#18224)
ea0b0099227 is described below
commit ea0b0099227a850254040ab87436eb5bbd092d60
Author: Caideyipi <[email protected]>
AuthorDate: Tue Jul 21 09:52:37 2026 +0800
Load: Fix active load reliability issues (#18224)
* Fix active load reliability issues
* Test active load file group isolation
* Address active load review comments
---
.../org/apache/iotdb/db/it/IoTDBLoadTsFileIT.java | 59 ++++++++
.../plan/node/load/LoadSingleTsFileNode.java | 28 ++--
.../plan/scheduler/load/LoadTsFileScheduler.java | 15 +-
.../load/active/ActiveLoadDirScanner.java | 11 +-
.../load/active/ActiveLoadPendingQueue.java | 20 ++-
.../load/active/ActiveLoadTsFileLoader.java | 40 +++++-
.../iotdb/db/storageengine/load/util/LoadUtil.java | 120 ++++++++++++----
.../plan/planner/node/load/LoadTsFileNodeTest.java | 46 ++++++
.../scheduler/load/LoadTsFileSchedulerTest.java | 42 ++++++
.../load/active/ActiveLoadDirScannerTest.java | 157 +++++++++++++++++++++
.../load/active/ActiveLoadTsFileLoaderTest.java | 20 +++
.../db/storageengine/load/util/LoadUtilTest.java | 141 ++++++++++++++++++
12 files changed, 649 insertions(+), 50 deletions(-)
diff --git
a/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBLoadTsFileIT.java
b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBLoadTsFileIT.java
index 1eda0f06226..8a0800771cf 100644
---
a/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBLoadTsFileIT.java
+++
b/integration-test/src/test/java/org/apache/iotdb/db/it/IoTDBLoadTsFileIT.java
@@ -20,8 +20,11 @@
package org.apache.iotdb.db.it;
import org.apache.iotdb.commons.auth.entity.PrivilegeType;
+import org.apache.iotdb.commons.path.MeasurementPath;
import org.apache.iotdb.commons.schema.column.ColumnHeaderConstant;
import org.apache.iotdb.db.it.utils.TestUtils;
+import
org.apache.iotdb.db.storageengine.dataregion.modification.ModificationFile;
+import
org.apache.iotdb.db.storageengine.dataregion.modification.TreeDeletionEntry;
import org.apache.iotdb.it.env.EnvFactory;
import org.apache.iotdb.it.env.cluster.node.DataNodeWrapper;
import org.apache.iotdb.it.framework.IoTDBTestRunner;
@@ -985,6 +988,62 @@ public class IoTDBLoadTsFileIT {
}
}
+ @Test
+ public void testAsyncLoadKeepsSameNamedTsFilesWithModsIsolated() throws
Exception {
+ registerSchema();
+
+ long expectedPointCount = 0;
+ // Before each file group had its own transfer directory, these same-named
TsFiles and mods
+ // were renamed independently in one shared directory and could be paired
with the wrong file.
+ for (int i = 0; i < 2; i++) {
+ final File sourceDir = new File(tmpDir, "source-" + i);
+ Assert.assertTrue(sourceDir.mkdirs());
+ try (final TsFileGenerator generator =
+ new TsFileGenerator(new File(sourceDir, "1-0-0-0.tsfile"))) {
+ generator.resetRandom(i);
+ generator.registerTimeseries(
+ SchemaConfig.DEVICE_0,
Collections.singletonList(SchemaConfig.MEASUREMENT_00));
+ generator.generateData(SchemaConfig.DEVICE_0, 20, 1, false,
TimeUnit.SECONDS.toMillis(i));
+ // Each group contributes 20 points and its own mods deletes exactly
one. Losing either
+ // TsFile-to-mods pairing therefore leaves 39 points instead of the
expected 38.
+ expectedPointCount += generator.getTotalNumber() - 1;
+ }
+ try (final ModificationFile modificationFile =
+ new ModificationFile(
+ new File(sourceDir, "1-0-0-0.tsfile" +
ModificationFile.FILE_SUFFIX), false)) {
+ modificationFile.write(
+ new TreeDeletionEntry(
+ new MeasurementPath(
+ SchemaConfig.DEVICE_0,
SchemaConfig.MEASUREMENT_00.getMeasurementName()),
+ TimeUnit.SECONDS.toMillis(i) + 1,
+ TimeUnit.SECONDS.toMillis(i) + 1));
+ }
+ }
+
+ try (final Connection connection = EnvFactory.getEnv().getConnection();
+ final Statement statement = connection.createStatement()) {
+ statement.execute(
+ String.format(
+ "load \"%s\" with
('async'='true','database-level'='2','on-success'='delete')",
+ tmpDir.getAbsolutePath()));
+ }
+
+ TestUtils.assertDataEventuallyOnEnv(
+ EnvFactory.getEnv(),
+ "select count("
+ + SchemaConfig.MEASUREMENT_00.getMeasurementName()
+ + ") from "
+ + SchemaConfig.DEVICE_0,
+ Collections.singletonMap(
+ "count("
+ + SchemaConfig.DEVICE_0
+ + "."
+ + SchemaConfig.MEASUREMENT_00.getMeasurementName()
+ + ")",
+ Long.toString(expectedPointCount)),
+ 30);
+ }
+
@Test
public void testLoadTsFileWithWrongTimestampPrecision() throws Exception {
try (final TsFileGenerator generator =
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/load/LoadSingleTsFileNode.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/load/LoadSingleTsFileNode.java
index dae0a63e974..ecaf0aea6ec 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/load/LoadSingleTsFileNode.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/planner/plan/node/load/LoadSingleTsFileNode.java
@@ -25,6 +25,7 @@ import org.apache.iotdb.common.rpc.thrift.TRegionReplicaSet;
import org.apache.iotdb.common.rpc.thrift.TTimePartitionSlot;
import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode;
import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
+import org.apache.iotdb.commons.utils.RetryUtils;
import org.apache.iotdb.commons.utils.TimePartitionUtils;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
@@ -241,17 +242,24 @@ public class LoadSingleTsFileNode extends WritePlanNode {
}
public void clean() {
+ if (!deleteAfterLoad) {
+ return;
+ }
+ deleteFile(tsFile);
+ deleteFile(new
File(LoadUtil.getTsFileResourcePath(tsFile.getAbsolutePath())));
+ deleteFile(ModificationFile.getExclusiveMods(tsFile));
+ deleteFile(new
File(LoadUtil.getTsFileModsV1Path(tsFile.getAbsolutePath())));
+ }
+
+ private void deleteFile(final File file) {
try {
- if (deleteAfterLoad) {
- Files.deleteIfExists(tsFile.toPath());
- Files.deleteIfExists(
- new
File(LoadUtil.getTsFileResourcePath(tsFile.getAbsolutePath())).toPath());
-
Files.deleteIfExists(ModificationFile.getExclusiveMods(tsFile).toPath());
- Files.deleteIfExists(
- new
File(LoadUtil.getTsFileModsV1Path(tsFile.getAbsolutePath())).toPath());
- }
- } catch (final IOException e) {
- LOGGER.warn(DataNodeQueryMessages.DELETE_AFTER_LOADING_ERROR, tsFile, e);
+ RetryUtils.retryOnException(
+ () -> {
+ Files.deleteIfExists(file.toPath());
+ return null;
+ });
+ } catch (final Exception e) {
+ LOGGER.warn(DataNodeQueryMessages.DELETE_AFTER_LOADING_ERROR, file, e);
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileScheduler.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileScheduler.java
index d3beefc0802..ef2e948c0e6 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileScheduler.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileScheduler.java
@@ -800,8 +800,7 @@ public class LoadTsFileScheduler implements IScheduler {
singleTsFileNode
.getTsFileResource()
.getTsFile()))); // can not just remove, because of
deletion
- dataSize -= pieceNode.getDataSize();
- block.reduceMemoryUsage(pieceNode.getDataSize());
+ releaseMemoryUsage(pieceNode.getDataSize());
if (!isDispatchSuccess) {
// Currently there is no retry, so return directly
@@ -886,7 +885,7 @@ public class LoadTsFileScheduler implements IScheduler {
boolean isAllSuccess = true;
for (Map.Entry<TConsensusGroupId, Pair<TRegionReplicaSet,
LoadTsFilePieceNode>> entry :
regionId2ReplicaSetAndNode.entrySet()) {
- block.reduceMemoryUsage(entry.getValue().getRight().getDataSize());
+ releaseMemoryUsage(entry.getValue().getRight().getDataSize());
if (isAllSuccess
&& !scheduler.dispatchOnePieceNode(
entry.getValue().getRight(), entry.getValue().getLeft())) {
@@ -900,7 +899,17 @@ public class LoadTsFileScheduler implements IScheduler {
return isAllSuccess;
}
+ private void releaseMemoryUsage(final long memorySize) {
+ dataSize -= memorySize;
+ block.reduceMemoryUsage(memorySize);
+ }
+
private void clear() {
+ if (dataSize > 0) {
+ block.reduceMemoryUsage(dataSize);
+ dataSize = 0;
+ }
+ nonDirectionalChunkData.clear();
regionId2ReplicaSetAndNode.clear();
}
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadDirScanner.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadDirScanner.java
index cb3460d78e7..9d1ad235ecb 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadDirScanner.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadDirScanner.java
@@ -108,21 +108,22 @@ public class ActiveLoadDirScanner extends
ActiveLoadScheduledExecutorService {
FileUtils.streamFiles(listeningDirFile, true, (String[]) null)) {
try {
fileStream
+ .map(file -> new
File(LoadUtil.getTsFilePath(file.getAbsolutePath())))
+ .distinct()
.filter(file ->
!activeLoadTsFileLoader.isFilePendingOrLoading(file))
.filter(File::exists)
- .map(file -> LoadUtil.getTsFilePath(file.getAbsolutePath()))
- .filter(this::isTsFileCompleted)
+ .filter(file -> isTsFileCompleted(file.getAbsolutePath()))
.limit(currentAllowedPendingSize)
.forEach(
- filePath -> {
- final File tsFile = new File(filePath);
+ tsFile -> {
final Map<String, String> attributes =
ActiveLoadPathHelper.parseAttributes(tsFile,
listeningDirFile);
final File parentFile = tsFile.getParentFile();
final boolean isTableModel =
ActiveLoadPathHelper.containsDatabaseName(attributes)
- || (parentFile != null
+ || (attributes.isEmpty()
+ && parentFile != null
&& !Objects.equals(
parentFile.getAbsoluteFile(),
listeningDirFile.getAbsoluteFile()));
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadPendingQueue.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadPendingQueue.java
index 88c18b19cb6..060935f51d0 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadPendingQueue.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadPendingQueue.java
@@ -61,9 +61,9 @@ public class ActiveLoadPendingQueue {
}
public synchronized void removeFromLoading(final String file) {
- loadingFileSet.remove(file);
-
-
ActiveLoadingFilesNumberMetricsSet.getInstance().increaseLoadingFileCounter(-1);
+ if (loadingFileSet.remove(file)) {
+
ActiveLoadingFilesNumberMetricsSet.getInstance().increaseLoadingFileCounter(-1);
+ }
}
public synchronized boolean isFilePendingOrLoading(final String file) {
@@ -78,6 +78,20 @@ public class ActiveLoadPendingQueue {
return pendingFileQueue.isEmpty() && loadingFileSet.isEmpty();
}
+ public synchronized void clear() {
+ final int loadingFileCount = loadingFileSet.size();
+ clearPending();
+ loadingFileSet.clear();
+
ActiveLoadingFilesNumberMetricsSet.getInstance().increaseLoadingFileCounter(-loadingFileCount);
+ }
+
+ public synchronized void clearPending() {
+ final int pendingFileCount = pendingFileSet.size();
+ pendingFileSet.clear();
+ pendingFileQueue.clear();
+
ActiveLoadingFilesNumberMetricsSet.getInstance().increaseQueuingFileCounter(-pendingFileCount);
+ }
+
public static class ActiveLoadEntry {
private final String file;
private final String pendingDir;
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoader.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoader.java
index ff5222e5754..1f0c1786608 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoader.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoader.java
@@ -160,12 +160,15 @@ public class ActiveLoadTsFileLoader {
public void stop() {
final WrappedThreadPoolExecutor executor =
activeLoadExecutor.getAndSet(null);
if (executor == null) {
+ pendingQueue.clearPending();
return;
}
- executor.shutdownNow();
+ boolean isTerminated = false;
try {
- if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
+ executor.shutdownNow();
+ isTerminated = executor.awaitTermination(30, TimeUnit.SECONDS);
+ if (!isTerminated) {
LOGGER.warn(
StorageEngineMessages.STILL_NOT_EXIT_AFTER_30S,
ThreadName.ACTIVE_LOAD_TSFILE_LOADER.getName());
@@ -175,6 +178,12 @@ public class ActiveLoadTsFileLoader {
StorageEngineMessages.STILL_NOT_EXIT_AFTER_30S,
ThreadName.ACTIVE_LOAD_TSFILE_LOADER.getName());
Thread.currentThread().interrupt();
+ } finally {
+ if (isTerminated) {
+ pendingQueue.clear();
+ } else {
+ pendingQueue.clearPending();
+ }
}
}
@@ -213,6 +222,7 @@ public class ActiveLoadTsFileLoader {
handleOtherException(loadEntry.get(), e);
} finally {
pendingQueue.removeFromLoading(loadEntry.get().getFile());
+ cleanupEmptyDirectories(loadEntry.get());
}
}
} finally {
@@ -364,6 +374,32 @@ public class ActiveLoadTsFileLoader {
}
}
+ private void cleanupEmptyDirectories(final
ActiveLoadPendingQueue.ActiveLoadEntry entry) {
+ final File pendingDir =
+ entry.getPendingDir() == null
+ ? ActiveLoadPathHelper.findPendingDirectory(new
File(entry.getFile()))
+ : new File(entry.getPendingDir());
+ if (pendingDir == null) {
+ return;
+ }
+
+ final Path pendingPath = pendingDir.toPath().toAbsolutePath().normalize();
+ Path currentPath = new
File(entry.getFile()).toPath().toAbsolutePath().normalize().getParent();
+ while (currentPath != null
+ && currentPath.startsWith(pendingPath)
+ && !currentPath.equals(pendingPath)) {
+ try {
+ Files.delete(currentPath);
+ } catch (final IOException e) {
+ if (Files.exists(currentPath)) {
+ LOGGER.debug(StorageEngineMessages.FAILED_DELETE_FOLDER_CLEANING_UP,
currentPath, e);
+ }
+ return;
+ }
+ currentPath = currentPath.getParent();
+ }
+ }
+
public boolean isFilePendingOrLoading(final File file) {
return pendingQueue.isFilePendingOrLoading(file.getAbsolutePath());
}
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/util/LoadUtil.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/util/LoadUtil.java
index 88c71fb1cef..4a67b362a5b 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/util/LoadUtil.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/util/LoadUtil.java
@@ -22,6 +22,7 @@ package org.apache.iotdb.db.storageengine.load.util;
import org.apache.iotdb.commons.disk.FolderManager;
import org.apache.iotdb.commons.disk.strategy.DirectoryStrategyType;
import org.apache.iotdb.commons.exception.DiskSpaceInsufficientException;
+import org.apache.iotdb.commons.utils.FileUtils;
import org.apache.iotdb.commons.utils.RetryUtils;
import org.apache.iotdb.db.auth.AuthorityChecker;
import org.apache.iotdb.db.conf.IoTDBDescriptor;
@@ -34,19 +35,22 @@ import
org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
import org.apache.iotdb.db.storageengine.load.active.ActiveLoadPathHelper;
import org.apache.iotdb.db.storageengine.load.disk.ILoadDiskSelector;
+import org.apache.tsfile.common.constant.TsFileConstant;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-
-import static org.apache.iotdb.commons.utils.FileUtils.copyFileWithMD5Check;
-import static org.apache.iotdb.commons.utils.FileUtils.moveFileWithMD5Check;
+import java.util.UUID;
public class LoadUtil {
@@ -130,13 +134,14 @@ public class LoadUtil {
final Map<String, String> attributes =
appendCurrentUserIfAbsent(loadAttributes);
final File targetDir =
ActiveLoadPathHelper.resolveTargetDir(targetFilePath, attributes);
- loadTsFileAsyncToTargetDir(
- targetDir, new File(getTsFileResourcePath(file.getAbsolutePath())),
isDeleteAfterLoad);
- loadTsFileAsyncToTargetDir(
- targetDir, new File(getTsFileModsV1Path(file.getAbsolutePath())),
isDeleteAfterLoad);
- loadTsFileAsyncToTargetDir(
- targetDir, new File(getTsFileModsV2Path(file.getAbsolutePath())),
isDeleteAfterLoad);
- loadTsFileAsyncToTargetDir(targetDir, file, isDeleteAfterLoad);
+ transferFilesToActiveDir(
+ targetDir,
+ Arrays.asList(
+ new File(getTsFileResourcePath(file.getAbsolutePath())),
+ new File(getTsFileModsV1Path(file.getAbsolutePath())),
+ new File(getTsFileModsV2Path(file.getAbsolutePath())),
+ file),
+ isDeleteAfterLoad);
return true;
}
@@ -183,32 +188,93 @@ public class LoadUtil {
final Map<String, String> attributes =
appendCurrentUserIfAbsent(loadAttributes);
final File targetDir =
ActiveLoadPathHelper.resolveTargetDir(targetFilePath, attributes);
+ final List<File> sourceFiles = new ArrayList<>(files.size());
for (final String file : files) {
- loadTsFileAsyncToTargetDir(targetDir, new File(file), isDeleteAfterLoad);
+ sourceFiles.add(new File(file));
}
+ sourceFiles.sort(Comparator.comparing(LoadUtil::isTsFile));
+ transferFilesToActiveDir(targetDir, sourceFiles, isDeleteAfterLoad);
return true;
}
- private static void loadTsFileAsyncToTargetDir(
- final File targetDir, final File file, final boolean isDeleteAfterLoad)
throws IOException {
- if (!file.exists()) {
+ static void transferFilesToActiveDir(
+ final File targetDir, final List<File> sourceFiles, final boolean
isDeleteAfterLoad)
+ throws IOException {
+ final List<File> existingSourceFiles = new ArrayList<>(sourceFiles.size());
+ for (final File sourceFile : sourceFiles) {
+ if (sourceFile.exists()) {
+ existingSourceFiles.add(sourceFile);
+ }
+ }
+ if (existingSourceFiles.isEmpty()) {
return;
}
- if (!targetDir.exists() && !targetDir.mkdirs()) {
- if (!targetDir.exists()) {
- throw new IOException(
- StorageEngineMessages.FAILED_TO_CREATE_TARGET_DIR +
targetDir.getAbsolutePath());
+
+ final File transferDir = new File(targetDir, UUID.randomUUID().toString());
+ try {
+ Files.createDirectories(transferDir.toPath());
+ for (final File sourceFile : existingSourceFiles) {
+ final File targetFile = new File(transferDir, sourceFile.getName());
+ RetryUtils.retryOnException(
+ () -> {
+ transferFile(sourceFile, targetFile, isDeleteAfterLoad);
+ return null;
+ });
}
+ } catch (final IOException | RuntimeException e) {
+ if (transferDir.exists()) {
+ FileUtils.deleteFileOrDirectoryWithRetry(transferDir);
+ }
+ throw e;
+ }
+
+ if (isDeleteAfterLoad) {
+ deleteSourceFiles(existingSourceFiles);
}
- RetryUtils.retryOnException(
- () -> {
- if (isDeleteAfterLoad) {
- moveFileWithMD5Check(file, targetDir);
- } else {
- copyFileWithMD5Check(file, targetDir);
- }
- return null;
- });
+ }
+
+ private static void transferFile(
+ final File sourceFile, final File targetFile, final boolean useHardLink)
throws IOException {
+ Exception linkException = null;
+ if (useHardLink) {
+ try {
+ Files.createLink(targetFile.toPath(), sourceFile.toPath());
+ return;
+ } catch (final IOException | UnsupportedOperationException |
SecurityException e) {
+ linkException = e;
+ }
+ }
+
+ try {
+ Files.copy(
+ sourceFile.toPath(),
+ targetFile.toPath(),
+ StandardCopyOption.REPLACE_EXISTING,
+ StandardCopyOption.COPY_ATTRIBUTES);
+ } catch (final IOException e) {
+ if (linkException != null) {
+ e.addSuppressed(linkException);
+ }
+ throw e;
+ }
+ }
+
+ private static void deleteSourceFiles(final List<File> sourceFiles) {
+ for (final File sourceFile : sourceFiles) {
+ try {
+ RetryUtils.retryOnException(
+ () -> {
+ Files.deleteIfExists(sourceFile.toPath());
+ return null;
+ });
+ } catch (final Exception e) {
+ LOGGER.warn(StorageEngineMessages.FAILED_TO_DELETE_FILE_OR_DIR,
sourceFile, e);
+ }
+ }
+ }
+
+ private static boolean isTsFile(final File file) {
+ return file.getName().endsWith(TsFileConstant.TSFILE_SUFFIX);
}
public static ILoadDiskSelector updateLoadDiskSelector() {
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/node/load/LoadTsFileNodeTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/node/load/LoadTsFileNodeTest.java
index e5898108946..37cedc50b34 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/node/load/LoadTsFileNodeTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/planner/node/load/LoadTsFileNodeTest.java
@@ -23,7 +23,9 @@ import
org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
import org.apache.iotdb.db.queryengine.plan.analyze.Analysis;
import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.load.LoadSingleTsFileNode;
import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.load.LoadTsFilePieceNode;
+import
org.apache.iotdb.db.storageengine.dataregion.modification.ModificationFile;
import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+import org.apache.iotdb.db.storageengine.load.util.LoadUtil;
import org.apache.tsfile.exception.NotImplementedException;
import org.junit.Assert;
@@ -31,6 +33,7 @@ import org.junit.Test;
import java.io.File;
import java.nio.ByteBuffer;
+import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
@@ -89,4 +92,47 @@ public class LoadTsFileNodeTest {
LoadTsFilePieceNode node1 = (LoadTsFilePieceNode)
LoadTsFilePieceNode.deserialize(buffer);
Assert.assertEquals(node.getTsFile(), node1.getTsFile());
}
+
+ @Test
+ public void testCleanContinuesAfterOneFileCannotBeDeleted() throws Exception
{
+ final File tempDir = Files.createTempDirectory("load-node-clean").toFile();
+ try {
+ // A non-empty directory at the TsFile path makes that deletion fail
deterministically. The
+ // companion cleanup must still continue instead of sharing the same
try-catch block.
+ final File tsFile = new File(tempDir, "1-0-0-0.tsfile");
+ Assert.assertTrue(tsFile.mkdirs());
+ Assert.assertTrue(new File(tsFile, "non-empty").createNewFile());
+ final File resourceFile = new
File(LoadUtil.getTsFileResourcePath(tsFile.getAbsolutePath()));
+ final File modsV2File = ModificationFile.getExclusiveMods(tsFile);
+ final File modsV1File = new
File(LoadUtil.getTsFileModsV1Path(tsFile.getAbsolutePath()));
+ Assert.assertTrue(resourceFile.createNewFile());
+ Assert.assertTrue(modsV2File.createNewFile());
+ Assert.assertTrue(modsV1File.createNewFile());
+
+ final LoadSingleTsFileNode node =
+ new LoadSingleTsFileNode(
+ new PlanNodeId(""), new TsFileResource(tsFile), false, null,
true, 0L, false);
+ node.clean();
+
+ Assert.assertTrue(tsFile.exists());
+ Assert.assertFalse(resourceFile.exists());
+ Assert.assertFalse(modsV2File.exists());
+ Assert.assertFalse(modsV1File.exists());
+ } finally {
+ deleteRecursively(tempDir);
+ }
+ }
+
+ private static void deleteRecursively(final File file) {
+ if (file == null || !file.exists()) {
+ return;
+ }
+ final File[] children = file.listFiles();
+ if (children != null) {
+ for (final File child : children) {
+ deleteRecursively(child);
+ }
+ }
+ Assert.assertTrue(file.delete());
+ }
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileSchedulerTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileSchedulerTest.java
index 19d97490c81..1b738f6496d 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileSchedulerTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileSchedulerTest.java
@@ -29,6 +29,7 @@ import
org.apache.iotdb.db.queryengine.plan.planner.plan.PlanFragment;
import org.apache.iotdb.db.queryengine.plan.planner.plan.SubPlan;
import
org.apache.iotdb.db.queryengine.plan.planner.plan.node.load.LoadSingleTsFileNode;
import org.apache.iotdb.db.queryengine.plan.statement.crud.LoadTsFileStatement;
+import
org.apache.iotdb.db.storageengine.load.memory.LoadTsFileDataCacheMemoryBlock;
import org.junit.Assert;
import org.junit.Before;
@@ -37,6 +38,8 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static org.mockito.Mockito.mock;
@@ -118,4 +121,43 @@ public class LoadTsFileSchedulerTest {
Assert.assertEquals(2, statement.getDatabaseLevel());
Assert.assertTrue(statement.isGeneratedByPipe());
}
+
+ @Test
+ public void testTsFileDataManagerClearReleasesCachedMemory() throws
Exception {
+ final Constructor<LoadTsFileDataCacheMemoryBlock> memoryBlockConstructor =
+
LoadTsFileDataCacheMemoryBlock.class.getDeclaredConstructor(long.class);
+ memoryBlockConstructor.setAccessible(true);
+ final LoadTsFileDataCacheMemoryBlock memoryBlock =
+ memoryBlockConstructor.newInstance(1024 * 1024L);
+
+ final Class<?> dataManagerClass =
+ Class.forName(LoadTsFileScheduler.class.getName() +
"$TsFileDataManager");
+ final Constructor<?> dataManagerConstructor =
+ dataManagerClass.getDeclaredConstructor(
+ LoadTsFileScheduler.class,
+ LoadSingleTsFileNode.class,
+ LoadTsFileDataCacheMemoryBlock.class);
+ dataManagerConstructor.setAccessible(true);
+ final Object dataManager =
+ dataManagerConstructor.newInstance(
+ mock(LoadTsFileScheduler.class), mock(LoadSingleTsFileNode.class),
memoryBlock);
+
+ // Simulate data buffered before split or routing aborts. clear() is the
last chance to return
+ // this accounting to the shared LOAD memory block.
+ final long cachedMemorySize = 128L;
+ memoryBlock.addMemoryUsage(cachedMemorySize);
+ final Field dataSizeField = dataManagerClass.getDeclaredField("dataSize");
+ dataSizeField.setAccessible(true);
+ dataSizeField.setLong(dataManager, cachedMemorySize);
+
+ final Method clearMethod = dataManagerClass.getDeclaredMethod("clear");
+ clearMethod.setAccessible(true);
+ clearMethod.invoke(dataManager);
+
+ final Method getMemoryUsageMethod =
+
LoadTsFileDataCacheMemoryBlock.class.getDeclaredMethod("getMemoryUsageInBytes");
+ getMemoryUsageMethod.setAccessible(true);
+ Assert.assertEquals(0L, getMemoryUsageMethod.invoke(memoryBlock));
+ Assert.assertEquals(0L, dataSizeField.getLong(dataManager));
+ }
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadDirScannerTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadDirScannerTest.java
new file mode 100644
index 00000000000..60532f749d8
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadDirScannerTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.load.active;
+
+import org.apache.iotdb.db.conf.IoTDBConfig;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import
org.apache.iotdb.db.storageengine.dataregion.modification.ModificationFile;
+import
org.apache.iotdb.db.storageengine.dataregion.modification.v1.ModificationFileV1;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+import org.apache.iotdb.db.storageengine.load.util.LoadUtil;
+
+import org.apache.tsfile.write.writer.TsFileIOWriter;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.nio.file.Files;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class ActiveLoadDirScannerTest {
+
+ private final IoTDBConfig config = IoTDBDescriptor.getInstance().getConfig();
+ private String[] originalListeningDirs;
+ private String originalPipeDir;
+ private boolean originalListeningEnabled;
+ private File tempDir;
+ private File pendingDir;
+ private File pipeDir;
+
+ @Before
+ public void setUp() throws Exception {
+ originalListeningDirs = config.getLoadActiveListeningDirs();
+ originalPipeDir = config.getLoadActiveListeningPipeDir();
+ originalListeningEnabled = config.getLoadActiveListeningEnable();
+
+ tempDir = Files.createTempDirectory("active-load-scanner").toFile();
+ pendingDir = new File(tempDir, "pending");
+ pipeDir = new File(tempDir, "pipe");
+ Assert.assertTrue(pendingDir.mkdirs());
+ Assert.assertTrue(pipeDir.mkdirs());
+ config.setLoadActiveListeningDirs(new String[]
{pendingDir.getAbsolutePath()});
+ config.setLoadActiveListeningPipeDir(pipeDir.getAbsolutePath());
+ config.setLoadActiveListeningEnable(true);
+ }
+
+ @After
+ public void tearDown() {
+ config.setLoadActiveListeningDirs(originalListeningDirs);
+ config.setLoadActiveListeningPipeDir(originalPipeDir);
+ config.setLoadActiveListeningEnable(originalListeningEnabled);
+ LoadUtil.updateLoadDiskSelector();
+ deleteRecursively(tempDir);
+ }
+
+ @Test
+ public void testScanDeduplicatesTsFileAndCompanionFiles() throws Exception {
+ // The recursive scanner sees all four paths, but every companion maps
back to the same TsFile.
+ // Enqueuing each path separately used to consume queue capacity and
schedule duplicate loads.
+ final File tsFile = createCompletedTsFile(pendingDir, "1-0-0-0.tsfile");
+ Assert.assertTrue(
+ new File(tsFile.getAbsolutePath() +
TsFileResource.RESOURCE_SUFFIX).createNewFile());
+ Assert.assertTrue(
+ new File(tsFile.getAbsolutePath() +
ModificationFileV1.FILE_SUFFIX).createNewFile());
+ Assert.assertTrue(
+ new File(tsFile.getAbsolutePath() +
ModificationFile.FILE_SUFFIX).createNewFile());
+
+ final ActiveLoadTsFileLoader loader = mock(ActiveLoadTsFileLoader.class);
+ when(loader.getCurrentAllowedPendingSize()).thenReturn(10);
+ final ActiveLoadDirScanner scanner = new ActiveLoadDirScanner(loader);
+ try {
+ final Method scanMethod =
ActiveLoadDirScanner.class.getDeclaredMethod("scan");
+ scanMethod.setAccessible(true);
+ scanMethod.invoke(scanner);
+ } finally {
+ scanner.stop();
+ }
+
+ verify(loader, times(1))
+ .tryTriggerTsFileLoad(
+ eq(tsFile.getAbsolutePath()), eq(pendingDir.getAbsolutePath()),
eq(false), eq(false));
+ }
+
+ @Test
+ public void testAttributeAndTransferDirectoriesDoNotImplyTableModel() throws
Exception {
+ // Async tree loads add attribute and per-handoff transfer directories
below pending. These are
+ // internal directories, not table database names inferred from a
user-created subdirectory.
+ final Map<String, String> attributes =
+ ActiveLoadPathHelper.buildAttributes(null, 2, false, false, null,
false, "test-user");
+ final File attributeDir =
ActiveLoadPathHelper.resolveTargetDir(pendingDir, attributes);
+ final File transferDir = new File(attributeDir, "transfer-id");
+ Assert.assertTrue(transferDir.mkdirs());
+ final File tsFile = createCompletedTsFile(transferDir, "2-0-0-0.tsfile");
+
+ final ActiveLoadTsFileLoader loader = mock(ActiveLoadTsFileLoader.class);
+ when(loader.getCurrentAllowedPendingSize()).thenReturn(10);
+ final ActiveLoadDirScanner scanner = new ActiveLoadDirScanner(loader);
+ try {
+ final Method scanMethod =
ActiveLoadDirScanner.class.getDeclaredMethod("scan");
+ scanMethod.setAccessible(true);
+ scanMethod.invoke(scanner);
+ } finally {
+ scanner.stop();
+ }
+
+ verify(loader, times(1))
+ .tryTriggerTsFileLoad(
+ eq(tsFile.getAbsolutePath()), eq(pendingDir.getAbsolutePath()),
eq(false), eq(false));
+ }
+
+ private static File createCompletedTsFile(final File dir, final String
fileName)
+ throws Exception {
+ final File tsFile = new File(dir, fileName);
+ try (final TsFileIOWriter writer = new TsFileIOWriter(tsFile)) {
+ writer.endFile();
+ }
+ return tsFile;
+ }
+
+ private static void deleteRecursively(final File file) {
+ if (file == null || !file.exists()) {
+ return;
+ }
+ final File[] children = file.listFiles();
+ if (children != null) {
+ for (final File child : children) {
+ deleteRecursively(child);
+ }
+ }
+ Assert.assertTrue(file.delete());
+ }
+}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoaderTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoaderTest.java
index 3208dc16ef5..aa81de4c53b 100644
---
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoaderTest.java
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/active/ActiveLoadTsFileLoaderTest.java
@@ -35,6 +35,7 @@ import org.junit.Before;
import org.junit.Test;
import java.io.File;
+import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
@@ -107,6 +108,25 @@ public class ActiveLoadTsFileLoaderTest {
Assert.assertTrue(new File(failDir, tsFile.getName() +
ModificationFile.FILE_SUFFIX).exists());
}
+ @Test
+ public void testStopClearsPendingFilesForRestart() throws Exception {
+ final ActiveLoadTsFileLoader loader = new ActiveLoadTsFileLoader();
+ final Field pendingQueueField =
ActiveLoadTsFileLoader.class.getDeclaredField("pendingQueue");
+ pendingQueueField.setAccessible(true);
+ final ActiveLoadPendingQueue pendingQueue =
+ (ActiveLoadPendingQueue) pendingQueueField.get(loader);
+ final String tsFilePath = new File(tempDir,
"pending.tsfile").getAbsolutePath();
+ Assert.assertTrue(pendingQueue.enqueue(tsFilePath,
tempDir.getAbsolutePath(), false, false));
+ Assert.assertTrue(loader.isFilePendingOrLoading(new File(tsFilePath)));
+
+ // A loader restart reuses this queue. Stale pending membership otherwise
makes the scanner
+ // believe the on-disk TsFile is already scheduled and it will never
enqueue it again.
+ loader.stop();
+
+ Assert.assertFalse(loader.isFilePendingOrLoading(new File(tsFilePath)));
+ Assert.assertTrue(pendingQueue.isEmpty());
+ }
+
private File createTsFileWithCompanionFiles(final String fileName) throws
Exception {
final File tsFile = new File(tempDir, fileName);
Assert.assertTrue(tsFile.createNewFile());
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/util/LoadUtilTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/util/LoadUtilTest.java
new file mode 100644
index 00000000000..f9537835da7
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/load/util/LoadUtilTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.load.util;
+
+import
org.apache.iotdb.db.storageengine.dataregion.modification.ModificationFile;
+import
org.apache.iotdb.db.storageengine.dataregion.modification.v1.ModificationFileV1;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class LoadUtilTest {
+
+ private File tempDir;
+ private File sourceDir;
+ private File targetDir;
+
+ @Before
+ public void setUp() throws Exception {
+ tempDir = Files.createTempDirectory("load-util").toFile();
+ sourceDir = new File(tempDir, "source");
+ targetDir = new File(tempDir, "target");
+ Assert.assertTrue(sourceDir.mkdirs());
+ Assert.assertTrue(targetDir.mkdirs());
+ }
+
+ @After
+ public void tearDown() {
+ deleteRecursively(tempDir);
+ }
+
+ @Test
+ public void
testTransferFilesKeepsSameNamedGroupsIsolatedAndDeletesSourcesAfterHandoff()
+ throws Exception {
+ final List<File> sourceFiles = createTsFileAndCompanions();
+ LoadUtil.transferFilesToActiveDir(targetDir, sourceFiles, true);
+
+ for (final File sourceFile : sourceFiles) {
+ Files.write(
+ sourceFile.toPath(), ("second-" +
sourceFile.getName()).getBytes(StandardCharsets.UTF_8));
+ }
+ LoadUtil.transferFilesToActiveDir(targetDir, sourceFiles, true);
+
+ final File[] transferDirs = targetDir.listFiles(File::isDirectory);
+ Assert.assertNotNull(transferDirs);
+ Assert.assertEquals(2, transferDirs.length);
+ for (final File sourceFile : sourceFiles) {
+ Assert.assertFalse(sourceFile.exists());
+ }
+
+ final Set<String> transferredPrefixes = new HashSet<>();
+ for (final File transferDir : transferDirs) {
+ final File tsFile = new File(transferDir, "1-0-0-0.tsfile");
+ final String tsFileContent =
+ new String(Files.readAllBytes(tsFile.toPath()),
StandardCharsets.UTF_8);
+ final String prefix = tsFileContent.startsWith("second-") ? "second-" :
"";
+ transferredPrefixes.add(prefix);
+ for (final File sourceFile : sourceFiles) {
+ final File transferredFile = new File(transferDir,
sourceFile.getName());
+ Assert.assertTrue(transferredFile.exists());
+ Assert.assertArrayEquals(
+ (prefix + sourceFile.getName()).getBytes(StandardCharsets.UTF_8),
+ Files.readAllBytes(transferredFile.toPath()));
+ }
+ }
+ Assert.assertEquals(new HashSet<>(Arrays.asList("", "second-")),
transferredPrefixes);
+ }
+
+ @Test
+ public void testTransferFailureDoesNotDeleteSources() throws Exception {
+ final List<File> sourceFiles = createTsFileAndCompanions();
+ // A regular file cannot contain the temporary transfer directory, forcing
handoff to fail
+ // before ownership of any source file can be released.
+ final File invalidTargetDir = new File(tempDir, "target-file");
+ Assert.assertTrue(invalidTargetDir.createNewFile());
+
+ try {
+ LoadUtil.transferFilesToActiveDir(invalidTargetDir, sourceFiles, true);
+ Assert.fail("Expected IOException");
+ } catch (final IOException ignored) {
+ // expected
+ }
+
+ for (final File sourceFile : sourceFiles) {
+ Assert.assertTrue(sourceFile.exists());
+ }
+ }
+
+ private List<File> createTsFileAndCompanions() throws Exception {
+ final File tsFile = new File(sourceDir, "1-0-0-0.tsfile");
+ final File resourceFile = new File(tsFile.getAbsolutePath() +
TsFileResource.RESOURCE_SUFFIX);
+ final File modsV1File = new File(tsFile.getAbsolutePath() +
ModificationFileV1.FILE_SUFFIX);
+ final File modsV2File = new File(tsFile.getAbsolutePath() +
ModificationFile.FILE_SUFFIX);
+ final List<File> sourceFiles = Arrays.asList(resourceFile, modsV1File,
modsV2File, tsFile);
+ for (final File sourceFile : sourceFiles) {
+ Files.write(sourceFile.toPath(),
sourceFile.getName().getBytes(StandardCharsets.UTF_8));
+ }
+ return sourceFiles;
+ }
+
+ private static void deleteRecursively(final File file) {
+ if (file == null || !file.exists()) {
+ return;
+ }
+ final File[] children = file.listFiles();
+ if (children != null) {
+ for (final File child : children) {
+ deleteRecursively(child);
+ }
+ }
+ Assert.assertTrue(file.delete());
+ }
+}