This is an automated email from the ASF dual-hosted git repository. jt2594838 pushed a commit to branch support_wal_in_import_data in repository https://gitbox.apache.org/repos/asf/iotdb.git
commit 2cd41b697d462a1cb9b48e2dfe7150f0d7a14904 Author: Tian Jiang <[email protected]> AuthorDate: Mon Aug 24 11:18:24 2026 +0800 add thread_num --- .../apache/iotdb/db/i18n/ImportWALMessages.java | 8 + .../apache/iotdb/db/i18n/ImportWALMessages.java | 8 + .../java/org/apache/iotdb/db/tools/ImportWAL.java | 437 ++++++++++++++++----- .../org/apache/iotdb/db/tools/ImportWALTest.java | 112 +++++- 4 files changed, 459 insertions(+), 106 deletions(-) diff --git a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/ImportWALMessages.java b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/ImportWALMessages.java index 0dd487bfe8b..3061a1845be 100644 --- a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/ImportWALMessages.java +++ b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/ImportWALMessages.java @@ -41,6 +41,9 @@ public final class ImportWALMessages { public static final String MESSAGE_WHEN_ALL_WAL_FILES_ARE_REPLAYED_SUCCESSFULLY_DO_OPERATION_ON_SOURCE_WAL_FILES_OPTIONAL_PARAMETERS_ARE_NONE_DEFAULT_AND_DELETE_41963A66 = "When all WAL files are replayed successfully, do operation on source WAL files. Optional parameters are none (default) and delete."; + public static final String + MESSAGE_NUMBER_OF_THREADS_USED_TO_REPLAY_WAL_DIRECTORIES_IN_PARALLEL_DEFAULT_1_6AEF4F50 = + "Number of threads used to replay WAL directories in parallel. Default: 1."; public static final String MESSAGE_PRINT_THIS_HELP_MESSAGE_E800AF7A = "Print this help message."; public static final String MESSAGE_ARGUMENT_ERROR_ARG_A9767F62 = "Argument error: %s"; @@ -52,6 +55,11 @@ public final class ImportWALMessages { public static final String EXCEPTION_NO_WAL_FILES_FOUND_UNDER_ARG_45F7FA22 = "No WAL files found under: %s"; public static final String EXCEPTION_INVALID_PORT_ARG_A7CDD5AC = "Invalid port: %s"; + public static final String + EXCEPTION_INVALID_THREAD_COUNT_ARG_EXPECTED_A_POSITIVE_INTEGER_F3AE2CFD = + "Invalid thread count: %s. Expected a positive integer."; + public static final String EXCEPTION_WAL_REPLAY_WAS_INTERRUPTED_770BA8AD = + "WAL replay was interrupted."; public static final String MESSAGE_REPLAYED_ARG_OPERATIONS_FROM_ARG_WAL_FILES_SKIPPED_ARG_ENTRIES_F0D37E3A = "Replayed %d operations from %d WAL files; skipped %d entries."; diff --git a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/ImportWALMessages.java b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/ImportWALMessages.java index 57052048e63..120c3265999 100644 --- a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/ImportWALMessages.java +++ b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/ImportWALMessages.java @@ -41,6 +41,9 @@ public final class ImportWALMessages { public static final String MESSAGE_WHEN_ALL_WAL_FILES_ARE_REPLAYED_SUCCESSFULLY_DO_OPERATION_ON_SOURCE_WAL_FILES_OPTIONAL_PARAMETERS_ARE_NONE_DEFAULT_AND_DELETE_41963A66 = "所有 WAL 文件成功重放后,对源 WAL 文件执行操作。可选参数为 none(默认)和 delete。"; + public static final String + MESSAGE_NUMBER_OF_THREADS_USED_TO_REPLAY_WAL_DIRECTORIES_IN_PARALLEL_DEFAULT_1_6AEF4F50 = + "并行重放 WAL 目录所用的线程数。默认:1。"; public static final String MESSAGE_PRINT_THIS_HELP_MESSAGE_E800AF7A = "打印帮助信息。"; public static final String MESSAGE_ARGUMENT_ERROR_ARG_A9767F62 = "参数错误:%s"; public static final String MESSAGE_WAL_IMPORT_FAILED_ARG_55C014BA = "WAL 导入失败:%s"; @@ -51,6 +54,11 @@ public final class ImportWALMessages { public static final String EXCEPTION_NO_WAL_FILES_FOUND_UNDER_ARG_45F7FA22 = "路径下未找到 WAL 文件:%s"; public static final String EXCEPTION_INVALID_PORT_ARG_A7CDD5AC = "无效端口:%s"; + public static final String + EXCEPTION_INVALID_THREAD_COUNT_ARG_EXPECTED_A_POSITIVE_INTEGER_F3AE2CFD = + "无效线程数:%s。应为正整数。"; + public static final String EXCEPTION_WAL_REPLAY_WAS_INTERRUPTED_770BA8AD = + "WAL 重放被中断。"; public static final String MESSAGE_REPLAYED_ARG_OPERATIONS_FROM_ARG_WAL_FILES_SKIPPED_ARG_ENTRIES_F0D37E3A = "已重放 %d 个操作(来自 %d 个 WAL 文件);跳过 %d 个条目。"; diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/tools/ImportWAL.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/tools/ImportWAL.java index c1281c00ee3..2214b69db92 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/tools/ImportWAL.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/tools/ImportWAL.java @@ -75,10 +75,17 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -89,7 +96,9 @@ public class ImportWAL { private static final String DEFAULT_HOST = "127.0.0.1"; private static final int DEFAULT_PORT = 6667; private static final String DEFAULT_USER = "root"; + private static final int DEFAULT_THREAD_NUM = 1; private static final int SNAPSHOT_TABLET_ROW_LIMIT = 1024; + private static final Object CONSOLE_PROMPT_LOCK = new Object(); private ImportWAL() {} @@ -120,53 +129,38 @@ public class ImportWAL { final String database = commandLine.getOptionValue("database"); final boolean deleteSource = shouldDeleteSource(commandLine.getOptionValue("on_success", "none")); + final int threadNum = + parseThreadNum( + commandLine.getOptionValue("thread_num", String.valueOf(DEFAULT_THREAD_NUM))); final String password = getPassword(commandLine); - final Session treeSession = - createSession( - commandLine.getOptionValue("host", DEFAULT_HOST), - parsePort(commandLine.getOptionValue("port", String.valueOf(DEFAULT_PORT))), - commandLine.getOptionValue("username", DEFAULT_USER), - password, - null); - final Session tableSession = - database == null - ? null - : createSession( - commandLine.getOptionValue("host", DEFAULT_HOST), - parsePort(commandLine.getOptionValue("port", String.valueOf(DEFAULT_PORT))), - commandLine.getOptionValue("username", DEFAULT_USER), - password, - database); - try { - treeSession.open(false); - if (tableSession != null) { - tableSession.open(false); - } - final ReplayStatistics statistics = - replayWALFiles( - walFiles, new WALReplayer(treeSession, tableSession, database), out, deleteSource); + final String host = commandLine.getOptionValue("host", DEFAULT_HOST); + final int port = parsePort(commandLine.getOptionValue("port", String.valueOf(DEFAULT_PORT))); + final String username = commandLine.getOptionValue("username", DEFAULT_USER); + final ReplayStatistics statistics = + replayWALDirectories( + walFiles, + threadNum, + () -> createWALReplayWorker(host, port, username, password, database), + out, + deleteSource); + out.printf( + ImportWALMessages + .MESSAGE_REPLAYED_ARG_OPERATIONS_FROM_ARG_WAL_FILES_SKIPPED_ARG_ENTRIES_F0D37E3A, + statistics.replayedOperationCount, + walFiles.size(), + statistics.skippedEntryCount); + out.println(); + out.printf( + ImportWALMessages + .MESSAGE_IMPORT_DURATION_ARG_SECONDS_TOTAL_SIZE_ARG_BYTES_AVERAGE_RATE_ARG_MB_PER_SECOND_4B4EA58D, + statistics.getElapsedSeconds(), + statistics.getTotalBytes(), + statistics.getAverageRateMbPerSecond()); + out.println(); + if (deleteSource) { out.printf( - ImportWALMessages - .MESSAGE_REPLAYED_ARG_OPERATIONS_FROM_ARG_WAL_FILES_SKIPPED_ARG_ENTRIES_F0D37E3A, - statistics.replayedOperationCount, - walFiles.size(), - statistics.skippedEntryCount); + ImportWALMessages.MESSAGE_DELETED_ARG_SOURCE_WAL_FILES_C7A5AA1B, walFiles.size()); out.println(); - out.printf( - ImportWALMessages - .MESSAGE_IMPORT_DURATION_ARG_SECONDS_TOTAL_SIZE_ARG_BYTES_AVERAGE_RATE_ARG_MB_PER_SECOND_4B4EA58D, - statistics.getElapsedSeconds(), - statistics.getTotalBytes(), - statistics.getAverageRateMbPerSecond()); - out.println(); - if (deleteSource) { - out.printf( - ImportWALMessages.MESSAGE_DELETED_ARG_SOURCE_WAL_FILES_C7A5AA1B, walFiles.size()); - out.println(); - } - } finally { - closeSession(tableSession); - closeSession(treeSession); } return CODE_OK; } catch (final Exception e) { @@ -228,6 +222,15 @@ public class ImportWAL { ImportWALMessages .MESSAGE_WHEN_ALL_WAL_FILES_ARE_REPLAYED_SUCCESSFULLY_DO_OPERATION_ON_SOURCE_WAL_FILES_OPTIONAL_PARAMETERS_ARE_NONE_DEFAULT_AND_DELETE_41963A66) .build()); + options.addOption( + Option.builder("tn") + .longOpt("thread_num") + .argName("thread_num") + .hasArg() + .desc( + ImportWALMessages + .MESSAGE_NUMBER_OF_THREADS_USED_TO_REPLAY_WAL_DIRECTORIES_IN_PARALLEL_DEFAULT_1_6AEF4F50) + .build()); options.addOption( Option.builder() .longOpt("help") @@ -314,6 +317,23 @@ public class ImportWAL { } } + static int parseThreadNum(final String threadNum) { + try { + final int value = Integer.parseInt(threadNum); + if (value <= 0) { + throw new NumberFormatException(threadNum); + } + return value; + } catch (final NumberFormatException e) { + throw new IllegalArgumentException( + String.format( + ImportWALMessages + .EXCEPTION_INVALID_THREAD_COUNT_ARG_EXPECTED_A_POSITIVE_INTEGER_F3AE2CFD, + threadNum), + e); + } + } + private static Session createSession( final String host, final int port, @@ -328,6 +348,29 @@ public class ImportWAL { return builder.build(); } + private static WALReplayWorker createWALReplayWorker( + final String host, + final int port, + final String username, + final String password, + final String database) + throws IOException { + final Session treeSession = createSession(host, port, username, password, null); + final Session tableSession = + database == null ? null : createSession(host, port, username, password, database); + try { + treeSession.open(false); + if (tableSession != null) { + tableSession.open(false); + } + return new SessionWALReplayer(treeSession, tableSession, database); + } catch (final IoTDBConnectionException e) { + closeSession(tableSession); + closeSession(treeSession); + throw new IOException(e.getMessage(), e); + } + } + private static void closeSession(final Session session) { if (session == null) { return; @@ -409,78 +452,219 @@ public class ImportWAL { final PrintStream progressStream, final boolean deleteSource) throws IOException { - final ReplayStatistics statistics = new ReplayStatistics(); final long startNanos = System.nanoTime(); + final ReplayStatistics statistics = createReplayStatistics(walFiles); for (final Path walFile : walFiles) { - statistics.totalBytes += Files.size(walFile); + recordCompletedFile( + statistics, + replayWALFile(walFile, replayer), + walFiles.size(), + startNanos, + progressStream); + } + if (deleteSource) { + deleteSourceWALFiles(walFiles); + } + return statistics; + } + + static ReplayStatistics replayWALDirectories( + final List<Path> walFiles, + final int threadNum, + final WALReplayWorkerFactory workerFactory, + final PrintStream progressStream, + final boolean deleteSource) + throws IOException { + if (threadNum <= 0) { + throw new IllegalArgumentException( + String.format( + ImportWALMessages + .EXCEPTION_INVALID_THREAD_COUNT_ARG_EXPECTED_A_POSITIVE_INTEGER_F3AE2CFD, + threadNum)); + } + final long startNanos = System.nanoTime(); + final ReplayStatistics statistics = createReplayStatistics(walFiles); + final List<List<Path>> walDirectories = groupWALFilesByDirectory(walFiles); + if (walDirectories.isEmpty()) { + statistics.elapsedNanos = System.nanoTime() - startNanos; + return statistics; + } + + final int workerCount = Math.min(threadNum, walDirectories.size()); + final AtomicInteger nextDirectoryIndex = new AtomicInteger(); + final AtomicReference<Throwable> replayFailure = new AtomicReference<>(); + final ExecutorService executor = Executors.newFixedThreadPool(workerCount); + final List<Future<?>> futures = new ArrayList<>(workerCount); + for (int i = 0; i < workerCount; i++) { + futures.add( + executor.submit( + () -> { + try (WALReplayWorker worker = workerFactory.create()) { + int directoryIndex; + // A worker owns one directory at a time so WAL files from that directory remain + // ordered, while independent directories can make progress concurrently. + while (replayFailure.get() == null + && (directoryIndex = nextDirectoryIndex.getAndIncrement()) + < walDirectories.size()) { + for (final Path walFile : walDirectories.get(directoryIndex)) { + recordCompletedFile( + statistics, + replayWALFile(walFile, worker), + walFiles.size(), + startNanos, + progressStream); + } + } + } catch (final Exception e) { + replayFailure.compareAndSet(null, e); + } + })); + } + executor.shutdown(); + + boolean interrupted = false; + for (final Future<?> future : futures) { + boolean completed = false; + // Wait for every worker even after a failure so sessions are closed before source deletion or + // the failure is reported to the caller. + while (!completed) { + try { + future.get(); + completed = true; + } catch (final InterruptedException e) { + interrupted = true; + replayFailure.compareAndSet( + null, + new IOException(ImportWALMessages.EXCEPTION_WAL_REPLAY_WAS_INTERRUPTED_770BA8AD, e)); + } catch (final ExecutionException e) { + replayFailure.compareAndSet(null, e.getCause()); + completed = true; + } + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + statistics.elapsedNanos = System.nanoTime() - startNanos; + rethrowReplayFailure(replayFailure.get()); + if (deleteSource) { + deleteSourceWALFiles(walFiles); + } + return statistics; + } + + static List<List<Path>> groupWALFilesByDirectory(final List<Path> walFiles) { + final List<Path> sortedWALFiles = new ArrayList<>(walFiles); + sortedWALFiles.sort(WAL_FILE_COMPARATOR); + final Map<Path, List<Path>> filesByDirectory = new LinkedHashMap<>(); + for (final Path walFile : sortedWALFiles) { + filesByDirectory + .computeIfAbsent(walFile.getParent(), ignored -> new ArrayList<>()) + .add(walFile); } + return new ArrayList<>(filesByDirectory.values()); + } + + private static ReplayStatistics createReplayStatistics(final List<Path> walFiles) + throws IOException { + final ReplayStatistics statistics = new ReplayStatistics(); for (final Path walFile : walFiles) { - try (WALReader reader = new WALReader(walFile.toFile())) { - long offset = reader.getWALCurrentReadOffset(); - while (reader.hasNext()) { - final WALEntry entry = reader.next(); - try { - if (replayer.replay(entry)) { - statistics.replayedOperationCount++; - } else { - statistics.skippedEntryCount++; - } - } catch (final IoTDBConnectionException | StatementExecutionException e) { - throw new WALReplayException( - String.format( - ImportWALMessages - .EXCEPTION_FAILED_TO_REPLAY_WAL_FILE_ARG_AT_OFFSET_ARG_ARG_FCFAF7F9, - walFile, - offset, - e.getMessage()), - e); + statistics.totalBytes += Files.size(walFile); + } + return statistics; + } + + private static ReplayStatistics replayWALFile(final Path walFile, final WALReplayWorker replayer) + throws IOException { + final ReplayStatistics statistics = new ReplayStatistics(); + try (WALReader reader = new WALReader(walFile.toFile())) { + long offset = reader.getWALCurrentReadOffset(); + while (reader.hasNext()) { + final WALEntry entry = reader.next(); + try { + if (replayer.replay(entry)) { + statistics.replayedOperationCount++; + } else { + statistics.skippedEntryCount++; } - offset = reader.getWALCurrentReadOffset(); - } - if (reader.isFileCorrupted()) { + } catch (final IoTDBConnectionException | StatementExecutionException e) { throw new WALReplayException( String.format( ImportWALMessages .EXCEPTION_FAILED_TO_REPLAY_WAL_FILE_ARG_AT_OFFSET_ARG_ARG_FCFAF7F9, walFile, - reader.getWALCurrentReadOffset(), - ImportWALMessages.EXCEPTION_THE_WAL_FILE_IS_TRUNCATED_OR_CORRUPTED_6B0734C5), - null); - } - statistics.completedFileCount++; - statistics.processedBytes += Files.size(walFile); - statistics.elapsedNanos = System.nanoTime() - startNanos; - if (progressStream != null) { - progressStream.printf( - ImportWALMessages - .MESSAGE_PROGRESS_ARG_COMPLETED_FILES_ARG_TOTAL_FILES_ARG_PROCESSED_BYTES_ARG_TOTAL_BYTES_ARG_PERCENT_ARG_ELAPSED_SECONDS_ARG_RATE_ARG_MB_PER_SECOND_F1C1356F, - statistics.completedFileCount, - walFiles.size(), - statistics.processedBytes, - statistics.totalBytes, - statistics.getProgressPercent(), - statistics.getElapsedSeconds(), - statistics.getAverageRateMbPerSecond()); - progressStream.println(); - } - } catch (final IOException e) { - if (e instanceof WALReplayException walReplayException) { - throw walReplayException; + offset, + e.getMessage()), + e); } + offset = reader.getWALCurrentReadOffset(); + } + if (reader.isFileCorrupted()) { throw new WALReplayException( String.format( ImportWALMessages .EXCEPTION_FAILED_TO_REPLAY_WAL_FILE_ARG_AT_OFFSET_ARG_ARG_FCFAF7F9, walFile, - 0, - e.getMessage()), - e); + reader.getWALCurrentReadOffset(), + ImportWALMessages.EXCEPTION_THE_WAL_FILE_IS_TRUNCATED_OR_CORRUPTED_6B0734C5), + null); } + statistics.completedFileCount++; + statistics.processedBytes += Files.size(walFile); + return statistics; + } catch (final IOException e) { + if (e instanceof WALReplayException walReplayException) { + throw walReplayException; + } + throw new WALReplayException( + String.format( + ImportWALMessages.EXCEPTION_FAILED_TO_REPLAY_WAL_FILE_ARG_AT_OFFSET_ARG_ARG_FCFAF7F9, + walFile, + 0, + e.getMessage()), + e); } - if (deleteSource) { - deleteSourceWALFiles(walFiles); + } + + private static void recordCompletedFile( + final ReplayStatistics statistics, + final ReplayStatistics completedFileStatistics, + final int totalFileCount, + final long startNanos, + final PrintStream progressStream) { + synchronized (statistics) { + statistics.add(completedFileStatistics); + statistics.elapsedNanos = System.nanoTime() - startNanos; + if (progressStream != null) { + progressStream.printf( + ImportWALMessages + .MESSAGE_PROGRESS_ARG_COMPLETED_FILES_ARG_TOTAL_FILES_ARG_PROCESSED_BYTES_ARG_TOTAL_BYTES_ARG_PERCENT_ARG_ELAPSED_SECONDS_ARG_RATE_ARG_MB_PER_SECOND_F1C1356F, + statistics.completedFileCount, + totalFileCount, + statistics.processedBytes, + statistics.totalBytes, + statistics.getProgressPercent(), + statistics.getElapsedSeconds(), + statistics.getAverageRateMbPerSecond()); + progressStream.println(); + } } - return statistics; + } + + private static void rethrowReplayFailure(final Throwable failure) throws IOException { + if (failure == null) { + return; + } + if (failure instanceof IOException ioException) { + throw ioException; + } + if (failure instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (failure instanceof Error error) { + throw error; + } + throw new IOException(failure); } private static void deleteSourceWALFiles(final List<Path> walFiles) throws IOException { @@ -498,7 +682,40 @@ public class ImportWAL { } } - static class WALReplayer { + @FunctionalInterface + interface WALReplayWorkerFactory { + + WALReplayWorker create() throws Exception; + } + + interface WALReplayWorker extends AutoCloseable { + + boolean replay(WALEntry entry) throws IoTDBConnectionException, StatementExecutionException; + + @Override + default void close() {} + } + + private static class SessionWALReplayer extends WALReplayer { + + private final Session treeSession; + private final Session tableSession; + + private SessionWALReplayer( + final Session treeSession, final Session tableSession, final String tableDatabaseName) { + super(treeSession, tableSession, tableDatabaseName); + this.treeSession = treeSession; + this.tableSession = tableSession; + } + + @Override + public void close() { + closeSession(tableSession); + closeSession(treeSession); + } + } + + static class WALReplayer implements WALReplayWorker { private final Session treeSession; private final Session tableSession; @@ -528,7 +745,8 @@ public class ImportWAL { null, null, ColumnFilterMatcher.matchAll(), tableDatabaseName); } - boolean replay(final WALEntry entry) + @Override + public boolean replay(final WALEntry entry) throws IoTDBConnectionException, StatementExecutionException { if (entry.getType() == WALEntryType.MEMORY_TABLE_SNAPSHOT || entry.getType() == WALEntryType.OLD_MEMORY_TABLE_SNAPSHOT) { @@ -558,12 +776,14 @@ public class ImportWAL { return null; } return entry -> { - final String answer = - console.readLine( - ImportWALMessages - .MESSAGE_UNSUPPORTED_WAL_OPERATION_ARG_SKIP_THIS_ENTRY_Y_N_DAFBE650, - entry.getType()); - return isSkipConfirmation(answer); + synchronized (CONSOLE_PROMPT_LOCK) { + final String answer = + console.readLine( + ImportWALMessages + .MESSAGE_UNSUPPORTED_WAL_OPERATION_ARG_SKIP_THIS_ENTRY_Y_N_DAFBE650, + entry.getType()); + return isSkipConfirmation(answer); + } }; } @@ -998,6 +1218,13 @@ public class ImportWAL { private long completedFileCount; private long elapsedNanos; + private void add(final ReplayStatistics statistics) { + replayedOperationCount += statistics.replayedOperationCount; + skippedEntryCount += statistics.skippedEntryCount; + processedBytes += statistics.processedBytes; + completedFileCount += statistics.completedFileCount; + } + long getReplayedOperationCount() { return replayedOperationCount; } diff --git a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/tools/ImportWALTest.java b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/tools/ImportWALTest.java index c8efb24ef9f..a40f020c18c 100644 --- a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/tools/ImportWALTest.java +++ b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/tools/ImportWALTest.java @@ -62,6 +62,12 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -70,6 +76,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -104,7 +111,7 @@ public class ImportWALTest { files); } - /** Covers CLI discovery of the source-file operation without opening a Session. */ + /** Covers CLI discovery of source-file and parallel replay options without opening a Session. */ @Test public void testHelpDescribesDeleteSourceOption() { final ByteArrayOutputStream output = new ByteArrayOutputStream(); @@ -114,6 +121,79 @@ public class ImportWALTest { assertEquals(0, exitCode); assertTrue(output.toString().contains("--on_success")); + assertTrue(output.toString().contains("--thread_num")); + } + + /** Covers valid thread counts and rejects zero, negative, and non-numeric values. */ + @Test + public void testParseThreadNumOption() { + assertEquals(1, ImportWAL.parseThreadNum("1")); + assertEquals(4, ImportWAL.parseThreadNum("4")); + assertThrows(IllegalArgumentException.class, () -> ImportWAL.parseThreadNum("0")); + assertThrows(IllegalArgumentException.class, () -> ImportWAL.parseThreadNum("-1")); + assertThrows(IllegalArgumentException.class, () -> ImportWAL.parseThreadNum("invalid")); + } + + /** + * Covers directory-level parallel replay with two WAL files per directory. Different directories + * must overlap, while versions within each directory must retain ascending replay order. + */ + @Test + public void testReplayWALDirectoriesInParallelAndPreserveDirectoryOrder() throws Exception { + final Path source = temporaryFolder.newFolder("parallel-wal-root").toPath(); + final Path nodeA = Files.createDirectory(source.resolve("node-a")); + final Path nodeB = Files.createDirectory(source.resolve("node-b")); + final Path a1 = createWALFile(nodeA, 1); + final Path a2 = createWALFile(nodeA, 2); + final Path b1 = createWALFile(nodeB, 1); + final Path b2 = createWALFile(nodeB, 2); + writeWAL(a1.toFile(), new WALInfoEntry(1, WALTestUtils.getInsertRowNode("root.sg.a", 1))); + writeWAL(a2.toFile(), new WALInfoEntry(2, WALTestUtils.getInsertRowNode("root.sg.a", 2))); + writeWAL(b1.toFile(), new WALInfoEntry(3, WALTestUtils.getInsertRowNode("root.sg.b", 1))); + writeWAL(b2.toFile(), new WALInfoEntry(4, WALTestUtils.getInsertRowNode("root.sg.b", 2))); + final List<Path> walFiles = ImportWAL.collectWALFiles(source); + final CyclicBarrier replayBarrier = new CyclicBarrier(2); + final AtomicInteger activeReplays = new AtomicInteger(); + final AtomicInteger maxActiveReplays = new AtomicInteger(); + final AtomicInteger createdWorkers = new AtomicInteger(); + final Map<String, List<Long>> replayedTimestamps = new ConcurrentHashMap<>(); + + final ImportWAL.ReplayStatistics statistics = + ImportWAL.replayWALDirectories( + walFiles, + 2, + () -> { + createdWorkers.incrementAndGet(); + final Session session = mock(Session.class); + doAnswer( + invocation -> { + final Tablet tablet = invocation.getArgument(0); + final int active = activeReplays.incrementAndGet(); + maxActiveReplays.accumulateAndGet(active, Math::max); + try { + replayBarrier.await(5, TimeUnit.SECONDS); + replayedTimestamps + .computeIfAbsent( + tablet.getDeviceId(), ignored -> new CopyOnWriteArrayList<>()) + .add(tablet.getTimestamp(0)); + } finally { + activeReplays.decrementAndGet(); + } + return null; + }) + .when(session) + .insertTablet(any(Tablet.class)); + return new ImportWAL.WALReplayer(session, null, null); + }, + null, + false); + + assertEquals(2, createdWorkers.get()); + assertTrue(maxActiveReplays.get() >= 2); + assertEquals(Arrays.asList(1L, 2L), replayedTimestamps.get("root.sg.a")); + assertEquals(Arrays.asList(1L, 2L), replayedTimestamps.get("root.sg.b")); + assertEquals(4, statistics.getReplayedOperationCount()); + assertEquals(4, statistics.getCompletedFileCount()); } /** Covers the default retention value, deletion value, normalization, and invalid input. */ @@ -191,6 +271,36 @@ public class ImportWALTest { assertTrue(corruptedWALFile.exists()); } + /** + * Covers all-or-nothing deletion during directory-level parallel replay. A corrupted directory + * must retain WAL files from both the failed directory and another concurrently replayed one. + */ + @Test + public void testParallelReplayRetainsAllSourceFilesWhenAnyDirectoryFails() throws Exception { + final Path source = temporaryFolder.newFolder("parallel-retain-wal-root").toPath(); + final Path validDirectory = Files.createDirectory(source.resolve("valid")); + final Path corruptedDirectory = Files.createDirectory(source.resolve("corrupted")); + final Path validWALFile = createWALFile(validDirectory, 0); + final Path corruptedWALFile = createWALFile(corruptedDirectory, 0); + writeWAL( + validWALFile.toFile(), + new WALInfoEntry(1, WALTestUtils.getInsertRowNode("root.sg.parallel.retain", 1))); + Files.write(corruptedWALFile, new byte[] {WALEntryType.INSERT_ROW_NODE.getCode()}); + + assertThrows( + IOException.class, + () -> + ImportWAL.replayWALDirectories( + ImportWAL.collectWALFiles(source), + 2, + () -> new ImportWAL.WALReplayer(mock(Session.class), null, null), + null, + true)); + + assertTrue(Files.exists(validWALFile)); + assertTrue(Files.exists(corruptedWALFile)); + } + @Test public void testReplayReportsProgressAndFileStatistics() throws Exception { final File walFile = createWALFile(0);
