diff --git a/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBQueryWithCorruptedTsFileIT.java b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBQueryWithCorruptedTsFileIT.java new file mode 100644 index 0000000000000..ae2fcb039930c --- /dev/null +++ b/integration-test/src/test/java/org/apache/iotdb/relational/it/query/recent/IoTDBQueryWithCorruptedTsFileIT.java @@ -0,0 +1,325 @@ +/* + * 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.relational.it.query.recent; + +import org.apache.iotdb.it.env.EnvFactory; +import org.apache.iotdb.it.framework.IoTDBTestRunner; +import org.apache.iotdb.itbase.category.TableLocalStandaloneIT; + +import org.apache.tsfile.common.conf.TSFileConfig; +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.exception.write.WriteProcessException; +import org.apache.tsfile.file.metadata.TableSchema; +import org.apache.tsfile.read.TsFileSequenceReader; +import org.apache.tsfile.write.TsFileWriter; +import org.apache.tsfile.write.record.Tablet; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.apache.iotdb.db.it.utils.TestUtils.tableAssertTestFail; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +@RunWith(IoTDBTestRunner.class) +@Category({TableLocalStandaloneIT.class}) +public class IoTDBQueryWithCorruptedTsFileIT { + private static final String DATABASE_NAME = "test_corrupted_read_tsfile"; + + private static File tmpDir; + + @BeforeClass + public static void setUp() throws Exception { + EnvFactory.getEnv().initClusterEnvironment(); + try (Connection connection = EnvFactory.getEnv().getTableConnection(); + Statement statement = connection.createStatement()) { + statement.execute("CREATE DATABASE " + DATABASE_NAME); + } + } + + @Before + public void setUpBeforeTest() throws IOException { + tmpDir = new File(Files.createTempDirectory("corrupt-tsfile").toUri()); + } + + @After + public void tearDownAfterTest() { + deleteTmpDir(); + } + + @AfterClass + public static void tearDown() { + EnvFactory.getEnv().cleanClusterEnvironment(); + } + + @Test + public void testReadTsFileWithCorruptedMetadataIndexNode() throws Exception { + File tsFile = new File(tmpDir, "corrupt-meta.tsfile"); + try (TsFileWriter writer = new TsFileWriter(tsFile)) { + generateTable( + writer, "table1", Arrays.asList("tag1"), Arrays.asList("s1"), TSDataType.INT64, 1, 10); + } + + // TsFile layout: [Header] [Data] [MetadataIndex Tree] [TsFileMetadata] [Magic][Size] + // ↑ metaOffset ↑ fileMetadataPos + // + // MetadataIndexNode serialization: + // [entryCount (varInt)] [entry1]...[entryN] [endOffset (long, 8B)] [nodeType (1B)] + // nodeType valid values: 0=INTERNAL_DEVICE, 1=LEAF_DEVICE, 2=INTERNAL_MEASUREMENT, + // 3=LEAF_MEASUREMENT + // nodeType is the LAST byte before TsFileMetadata, i.e. at fileMetadataPos - 1 + long metaOffset; + long fileMetadataPos; + try (TsFileSequenceReader reader = new TsFileSequenceReader(tsFile.getAbsolutePath())) { + metaOffset = reader.readFileMetadata().getMetaOffset(); + fileMetadataPos = reader.getFileMetadataPos(); + } + + // Corrupt the nodeType byte to 0xFF (all valid types are 0-3) + byte[] fileBytes = Files.readAllBytes(tsFile.toPath()); + fileBytes[(int) fileMetadataPos - 1] = (byte) 0xFF; + Files.write(tsFile.toPath(), fileBytes); + + tableAssertTestFail( + "SELECT * FROM read_tsfile(PATHS => '" + toSqlPath(tsFile) + "')", + "timeseries metadata", + DATABASE_NAME); + } + + @Test + public void testReadTsFileWithCorruptedPageData() throws Exception { + File tsFile = new File(tmpDir, "corrupt-page.tsfile"); + try (TsFileWriter writer = new TsFileWriter(tsFile)) { + generateTable( + writer, "table1", Arrays.asList("tag1"), Arrays.asList("s1"), TSDataType.INT64, 1, 100); + } + + corruptDataSection(tsFile); + + tableAssertTestFail( + "SELECT * FROM read_tsfile(PATHS => '" + toSqlPath(tsFile) + "')", "TsFile", DATABASE_NAME); + } + + @Test + public void testNormalQueryWithCorruptedPageData() throws Exception { + String tableName = "corrupt_table"; + // 1. Create table and insert data via session — generates TsFiles in the data directory + try (Connection connection = EnvFactory.getEnv().getTableConnection(); + Statement statement = connection.createStatement()) { + statement.execute("USE " + DATABASE_NAME); + statement.execute( + "CREATE TABLE " + tableName + "(device_id STRING TAG, s1 INT64 FIELD, s2 INT64 FIELD)"); + for (int i = 1; i <= 200; i++) { + statement.execute( + "INSERT INTO " + + tableName + + "(time, device_id, s1, s2) VALUES(" + + i + + ", 'd" + + (i % 10) + + "', " + + i + + ", " + + (i * 10) + + ")"); + } + statement.execute("FLUSH"); + } + + // 2. Find the generated TsFile in the data directory + File sequenceDir = + new File( + EnvFactory.getEnv().getDataNodeWrapper(0).getDataNodeDir() + + File.separator + + "data" + + File.separator + + "sequence"); + File tsFile = findTsFileRecursively(sequenceDir); + if (tsFile == null) { + fail("Could not find TsFile in data directory: " + sequenceDir.getAbsolutePath()); + } + + // 3. Corrupt the data section of the TsFile + corruptDataSection(tsFile); + + // 4. Query — should fail with a corruption message that does NOT include the file path + try (Connection connection = EnvFactory.getEnv().getTableConnection(); + Statement statement = connection.createStatement()) { + statement.execute("USE " + DATABASE_NAME); + try { + statement.execute("SELECT * FROM " + tableName + " ORDER BY time"); + fail("Expected query on corrupted TsFile to fail"); + } catch (SQLException e) { + assertTrue( + "Error message should mention corruption without file path: " + e.getMessage(), + e.getMessage().contains("may be corrupted") + || e.getMessage().contains("check the logs")); + } + } + } + + /** + * Corrupts a block of bytes in the data section of a TsFile (between the header and the + * MetadataIndex tree). Uses {@link TsFileSequenceReader} to read {@code metaOffset} from + * TsFileMetadata, so corruption reliably hits compressed page data rather than metadata. + * + *

TsFile layout: [Header] [Data chunks] [MetadataIndex Tree] [TsFileMetadata] [Magic][Size] ↑ + * metaOffset + */ + private static void corruptDataSection(File tsFile) throws IOException { + long metaOffset; + try (TsFileSequenceReader reader = new TsFileSequenceReader(tsFile.getAbsolutePath())) { + metaOffset = reader.readFileMetadata().getMetaOffset(); + } + + byte[] fileBytes = Files.readAllBytes(tsFile.toPath()); + int magicLen = TSFileConfig.MAGIC_STRING.getBytes().length; + int dataStart = magicLen + Byte.BYTES; + int dataEnd = (int) metaOffset; + // Corrupt bytes in the middle of the data section — XOR 512 bytes to ensure decompression fails + int middle = dataStart + (dataEnd - dataStart) / 2; + int corruptLen = Math.min(512, dataEnd - middle); + for (int i = 0; i < corruptLen; i++) { + fileBytes[middle + i] ^= 0xFF; + } + Files.write(tsFile.toPath(), fileBytes); + } + + private static File findTsFileRecursively(File dir) { + if (dir == null || !dir.exists()) { + return null; + } + File[] files = dir.listFiles(); + if (files == null) { + return null; + } + for (File file : files) { + if (file.isDirectory()) { + File found = findTsFileRecursively(file); + if (found != null) { + return found; + } + } else if (file.getName().endsWith(".tsfile") && file.length() > 0) { + return file; + } + } + return null; + } + + private static void generateTable( + TsFileWriter writer, + String tableName, + List tagColumns, + List fieldColumns, + TSDataType fieldType, + long startTime, + long endTime) + throws IOException, WriteProcessException { + List columnNames = new ArrayList<>(tagColumns.size() + fieldColumns.size()); + List columnTypes = new ArrayList<>(tagColumns.size() + fieldColumns.size()); + List columnCategories = + new ArrayList<>(tagColumns.size() + fieldColumns.size()); + for (String tagColumn : tagColumns) { + columnNames.add(tagColumn); + columnTypes.add(TSDataType.STRING); + columnCategories.add(ColumnCategory.TAG); + } + for (String fieldColumn : fieldColumns) { + columnNames.add(fieldColumn); + columnTypes.add(fieldType); + columnCategories.add(ColumnCategory.FIELD); + } + + writer.registerTableSchema( + new TableSchema(tableName, columnNames, columnTypes, columnCategories)); + Tablet tablet = new Tablet(tableName, columnNames, columnTypes, columnCategories); + for (int deviceIndex = 1; deviceIndex <= 2; deviceIndex++) { + for (long time = startTime; time <= endTime; time++) { + int row = tablet.getRowSize(); + tablet.addTimestamp(row, time); + for (int i = 0; i < tagColumns.size(); i++) { + tablet.addValue(row, i, tagColumns.get(i) + "_" + deviceIndex); + } + for (int i = 0; i < fieldColumns.size(); i++) { + tablet.addValue(row, tagColumns.size() + i, time); + } + if (tablet.getRowSize() == tablet.getMaxRowNumber()) { + writer.writeTable(tablet); + tablet.reset(); + } + } + } + if (tablet.getRowSize() != 0) { + writer.writeTable(tablet); + } + } + + private static String toSqlPath(File file) { + return file.getAbsolutePath().replace("\\", "\\\\").replace("'", "''"); + } + + private static void deleteTmpDir() { + if (tmpDir == null || !tmpDir.exists()) { + return; + } + File[] files = tmpDir.listFiles(); + if (files != null) { + for (File file : files) { + deleteRecursively(file); + } + } + try { + Files.delete(tmpDir.toPath()); + } catch (IOException ignored) { + // ignore + } + } + + private static void deleteRecursively(File file) { + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + deleteRecursively(child); + } + } + } + try { + Files.delete(file.toPath()); + } catch (IOException ignored) { + // ignore + } + } +} diff --git a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java index 92242b62aa726..5839542d1b3d4 100644 --- a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java +++ b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java @@ -218,6 +218,32 @@ public final class DataNodeQueryMessages { "Error happened while scanning the file"; public static final String ALL_CACHED_CHUNKS_SHOULD_BE_CONSUMED_FIRST = "all cached chunks should be consumed first"; + public static final String + EXCEPTION_FAILED_TO_READ_TIMESERIES_METADATA_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_0B9E652E = + "Failed to read timeseries metadata. The TsFile may be corrupted," + + " please check the logs for the corrupted file path."; + public static final String EXCEPTION_FAILED_TO_READ_TIMESERIES_METADATA_FROM_TSFILE_ARG_B07568F8 = + "Failed to read timeseries metadata from TsFile: %s"; + public static final String + EXCEPTION_FAILED_TO_READ_CHUNK_DATA_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_F0FFE629 = + "Failed to read chunk data. The TsFile may be corrupted," + + " please check the logs for the corrupted file path."; + public static final String EXCEPTION_FAILED_TO_READ_CHUNK_DATA_FROM_TSFILE_ARG_B88F2496 = + "Failed to read chunk data from TsFile: %s"; + public static final String + EXCEPTION_FAILED_TO_DECODE_PAGE_DATA_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_54D7C6D9 = + "Failed to decode page data. The TsFile may be corrupted," + + " please check the logs for the corrupted file path."; + public static final String EXCEPTION_FAILED_TO_DECODE_PAGE_DATA_FROM_TSFILE_ARG_645F5377 = + "Failed to decode page data from TsFile: %s"; + public static final String + EXCEPTION_FAILED_TO_LOAD_PAGE_READER_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_05D35760 = + "Failed to load page reader. The TsFile may be corrupted," + + " please check the logs for the corrupted file path."; + public static final String EXCEPTION_FAILED_TO_LOAD_PAGE_READER_FROM_TSFILE_ARG_3B1CCC18 = + "Failed to load page reader from TsFile: %s"; + public static final String EXCEPTION_FAILED_TO_READ_METADATA_INDEX_NODE_FROM_TSFILE_ARG_EC5B6633 = + "Failed to read metadata index node from TsFile: %s"; public static final String OVERLAPPED_DATA_SHOULD_BE_CONSUMED_FIRST = "overlapped data should be consumed first"; public static final String NO_MORE_BATCH_DATA = diff --git a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java index 9ce80200a7b75..2ba8eb4162b84 100644 --- a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java +++ b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java @@ -205,6 +205,28 @@ public final class DataNodeQueryMessages { "扫描文件时发生错误"; public static final String ERROR_HAPPENED_WHILE_SCANNING_THE_FILE = "扫描文件时发生错误"; + public static final String + EXCEPTION_FAILED_TO_READ_TIMESERIES_METADATA_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_0B9E652E = + "读取时间序列元数据失败。TsFile 可能已损坏,请检查日志中的损坏文件路径。"; + public static final String EXCEPTION_FAILED_TO_READ_TIMESERIES_METADATA_FROM_TSFILE_ARG_B07568F8 = + "从 TsFile 读取时间序列元数据失败:%s"; + public static final String + EXCEPTION_FAILED_TO_READ_CHUNK_DATA_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_F0FFE629 = + "读取 chunk 数据失败。TsFile 可能已损坏,请检查日志中的损坏文件路径。"; + public static final String EXCEPTION_FAILED_TO_READ_CHUNK_DATA_FROM_TSFILE_ARG_B88F2496 = + "从 TsFile 读取 chunk 数据失败:%s"; + public static final String + EXCEPTION_FAILED_TO_DECODE_PAGE_DATA_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_54D7C6D9 = + "解码 page 数据失败。TsFile 可能已损坏,请检查日志中的损坏文件路径。"; + public static final String EXCEPTION_FAILED_TO_DECODE_PAGE_DATA_FROM_TSFILE_ARG_645F5377 = + "从 TsFile 解码 page 数据失败:%s"; + public static final String + EXCEPTION_FAILED_TO_LOAD_PAGE_READER_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_05D35760 = + "加载 page reader 失败。TsFile 可能已损坏,请检查日志中的损坏文件路径。"; + public static final String EXCEPTION_FAILED_TO_LOAD_PAGE_READER_FROM_TSFILE_ARG_3B1CCC18 = + "从 TsFile 加载 page reader 失败:%s"; + public static final String EXCEPTION_FAILED_TO_READ_METADATA_INDEX_NODE_FROM_TSFILE_ARG_EC5B6633 = + "从 TsFile 读取元数据索引节点失败:%s"; public static final String ALL_CACHED_CHUNKS_SHOULD_BE_CONSUMED_FIRST = "所有缓存的 chunk 应先被消费"; public static final String OVERLAPPED_DATA_SHOULD_BE_CONSUMED_FIRST = diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/exception/CorruptedTsFileException.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/exception/CorruptedTsFileException.java new file mode 100644 index 0000000000000..6574b3a042dc8 --- /dev/null +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/exception/CorruptedTsFileException.java @@ -0,0 +1,74 @@ +/* + * 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.exception; + +import org.apache.tsfile.exception.TsFileRuntimeException; + +import java.io.File; + +/** + * Thrown when a TsFile is detected to be corrupted during query execution. Extends {@link + * TsFileRuntimeException} so it follows the existing TsFile error handling and bypasses all {@code + * catch (IOException)} blocks in the operator hierarchy. + */ +public class CorruptedTsFileException extends TsFileRuntimeException { + + public enum Stage { + READ_TIMESERIES_METADATA, + READ_CHUNK_DATA, + LOAD_PAGE_READER, + DECODE_PAGE_DATA, + READ_METADATA_INDEX_NODE + } + + private final File tsFile; + private final Stage stage; + + /** + * Creates a CorruptedTsFileException. + * + *

The original exception {@code cause} is added via {@link #addSuppressed(Throwable)} rather + * than {@link #initCause(Throwable)}. This ensures that {@code + * ErrorHandlingCommonUtils.getRootCause(this)} returns this exception itself (not the wrapped + * IOException), so upstream error handling in {@code AbstractDriverThread} matches it correctly. + * The original cause stack trace is preserved via suppressed exceptions. + * + * @param tsFile the corrupted TsFile + * @param stage the operation that encountered the corruption + * @param message user-facing error message + * @param cause the original exception (preserved as suppressed) + */ + public CorruptedTsFileException(File tsFile, Stage stage, String message, Throwable cause) { + super(message); + this.tsFile = tsFile; + this.stage = stage; + if (cause != null) { + addSuppressed(cause); + } + } + + public File getTsFile() { + return tsFile; + } + + public Stage getStage() { + return stage; + } +} diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/FileLoaderUtils.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/FileLoaderUtils.java index 2a0aa01f6d9a4..810d9f54c62df 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/FileLoaderUtils.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/FileLoaderUtils.java @@ -22,6 +22,7 @@ import org.apache.iotdb.commons.path.AlignedFullPath; import org.apache.iotdb.commons.path.NonAlignedFullPath; import org.apache.iotdb.db.exception.ChunkTypeInconsistentException; +import org.apache.iotdb.db.exception.CorruptedTsFileException; import org.apache.iotdb.db.i18n.DataNodeQueryMessages; import org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceContext; import org.apache.iotdb.db.queryengine.execution.fragment.QueryContext; @@ -54,6 +55,7 @@ import org.apache.tsfile.read.reader.IPageReader; import org.apache.tsfile.write.schema.IMeasurementSchema; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; @@ -163,6 +165,18 @@ public static TimeseriesMetadata loadTimeSeriesMetadata( } return timeSeriesMetadata; + } catch (Exception e) { + throw new CorruptedTsFileException( + resource.getTsFile(), + CorruptedTsFileException.Stage.READ_TIMESERIES_METADATA, + context.isExternalTsFileScan() + ? String.format( + DataNodeQueryMessages + .EXCEPTION_FAILED_TO_READ_TIMESERIES_METADATA_FROM_TSFILE_ARG_B07568F8, + resource.getTsFile()) + : DataNodeQueryMessages + .EXCEPTION_FAILED_TO_READ_TIMESERIES_METADATA_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_0B9E652E, + e); } finally { long costTime = System.nanoTime() - t1; if (loadFromMem) { @@ -250,6 +264,18 @@ public static AbstractAlignedTimeSeriesMetadata loadAlignedTimeSeriesMetadata( } } return alignedTimeSeriesMetadata; + } catch (Exception e) { + throw new CorruptedTsFileException( + resource.getTsFile(), + CorruptedTsFileException.Stage.READ_TIMESERIES_METADATA, + context.isExternalTsFileScan() + ? String.format( + DataNodeQueryMessages + .EXCEPTION_FAILED_TO_READ_TIMESERIES_METADATA_FROM_TSFILE_ARG_B07568F8, + resource.getTsFile()) + : DataNodeQueryMessages + .EXCEPTION_FAILED_TO_READ_TIMESERIES_METADATA_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_0B9E652E, + e); } finally { long costTime = System.nanoTime() - t1; if (loadFromMem) { @@ -469,25 +495,66 @@ public static List loadChunkMetadataList(ITimeSeriesMetadata tim * IOException will be thrown */ public static List loadPageReaderList( - IChunkMetadata chunkMetaData, Filter globalTimeFilter, List targetDataTypeList) + IChunkMetadata chunkMetaData, + Filter globalTimeFilter, + List targetDataTypeList, + FragmentInstanceContext context) throws IOException { checkArgument( chunkMetaData != null, DataNodeQueryMessages.EXCEPTION_CAN_QUOTE_T_INIT_NULL_CHUNKMETA_15C12BEE); IChunkLoader chunkLoader = chunkMetaData.getChunkLoader(); - IChunkReader chunkReader; + File tsFile = null; + if (chunkLoader instanceof DiskChunkLoader) { + tsFile = ((DiskChunkLoader) chunkLoader).getTsFile(); + } else if (chunkLoader instanceof DiskAlignedChunkLoader) { + tsFile = ((DiskAlignedChunkLoader) chunkLoader).getTsFile(); + } + final IChunkReader chunkReader; try { chunkReader = chunkLoader.getChunkReader(chunkMetaData, globalTimeFilter); } catch (ChunkTypeInconsistentException e) { // if the chunk in tsfile is a value chunk of aligned series but registered series is // non-aligned, we should skip all data of this chunk. return Collections.emptyList(); + } catch (Exception e) { + if (tsFile == null) { + throw e; + } + throw new CorruptedTsFileException( + tsFile, + CorruptedTsFileException.Stage.READ_CHUNK_DATA, + context.isExternalTsFileScan() + ? String.format( + DataNodeQueryMessages + .EXCEPTION_FAILED_TO_READ_CHUNK_DATA_FROM_TSFILE_ARG_B88F2496, + tsFile) + : DataNodeQueryMessages + .EXCEPTION_FAILED_TO_READ_CHUNK_DATA_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_F0FFE629, + e); } if (chunkMetaData.isDataTypeModifiedAndCannotUseStatistics()) { chunkReader.markDataTypeModifiedAndCannotUseStatistics(); } - return chunkReader.loadPageReaderList(); + try { + return chunkReader.loadPageReaderList(); + } catch (Exception e) { + if (tsFile == null) { + throw e; + } + throw new CorruptedTsFileException( + tsFile, + CorruptedTsFileException.Stage.LOAD_PAGE_READER, + context.isExternalTsFileScan() + ? String.format( + DataNodeQueryMessages + .EXCEPTION_FAILED_TO_LOAD_PAGE_READER_FROM_TSFILE_ARG_3B1CCC18, + tsFile) + : DataNodeQueryMessages + .EXCEPTION_FAILED_TO_LOAD_PAGE_READER_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_05D35760, + e); + } } /** diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtil.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtil.java index 52ddf505a5e54..1b8ca2b1020c1 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtil.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/operator/source/SeriesScanUtil.java @@ -22,6 +22,7 @@ import org.apache.iotdb.calc.execution.filter.TopKRuntimeFilter; import org.apache.iotdb.commons.path.IFullPath; import org.apache.iotdb.commons.path.NonAlignedFullPath; +import org.apache.iotdb.db.exception.CorruptedTsFileException; import org.apache.iotdb.db.i18n.DataNodeQueryMessages; import org.apache.iotdb.db.queryengine.execution.fragment.FragmentInstanceContext; import org.apache.iotdb.db.queryengine.execution.fragment.QueryContext; @@ -32,6 +33,8 @@ import org.apache.iotdb.db.storageengine.dataregion.memtable.AlignedReadOnlyMemChunk; import org.apache.iotdb.db.storageengine.dataregion.memtable.ReadOnlyMemChunk; import org.apache.iotdb.db.storageengine.dataregion.read.QueryDataSource; +import org.apache.iotdb.db.storageengine.dataregion.read.reader.chunk.DiskAlignedChunkLoader; +import org.apache.iotdb.db.storageengine.dataregion.read.reader.chunk.DiskChunkLoader; import org.apache.iotdb.db.storageengine.dataregion.read.reader.chunk.MemAlignedPageReader; import org.apache.iotdb.db.storageengine.dataregion.read.reader.chunk.MemChunkLoader; import org.apache.iotdb.db.storageengine.dataregion.read.reader.chunk.MemPageReader; @@ -79,6 +82,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.File; import java.io.IOException; import java.io.Serializable; import java.nio.charset.StandardCharsets; @@ -695,6 +699,14 @@ private void unpackOneChunkMetaData(IChunkMetadata chunkMetaData) throws IOExcep long timestampInFileName = FileLoaderUtils.getTimestampInFileName(chunkMetaData); IChunkLoader chunkLoader = chunkMetaData.getChunkLoader(); + final File tsFile; + if (chunkLoader instanceof DiskChunkLoader) { + tsFile = ((DiskChunkLoader) chunkLoader).getTsFile(); + } else if (chunkLoader instanceof DiskAlignedChunkLoader) { + tsFile = ((DiskAlignedChunkLoader) chunkLoader).getTsFile(); + } else { + tsFile = null; + } if ((chunkLoader instanceof MemChunkLoader) && ((MemChunkLoader) chunkLoader).isStreamingQueryMemChunk()) { unpackOneFakeMemChunkMetaData( @@ -703,7 +715,7 @@ private void unpackOneChunkMetaData(IChunkMetadata chunkMetaData) throws IOExcep } List pageReaderList = FileLoaderUtils.loadPageReaderList( - chunkMetaData, scanOptions.getGlobalTimeFilter(), getTsDataTypeList()); + chunkMetaData, scanOptions.getGlobalTimeFilter(), getTsDataTypeList(), context); // init TsBlockBuilder for each page reader pageReaderList.forEach(p -> p.initTsBlockBuilder(getTsDataTypeList())); @@ -718,7 +730,8 @@ private void unpackOneChunkMetaData(IChunkMetadata chunkMetaData) throws IOExcep chunkMetaData.getVersion(), chunkMetaData.getOffsetOfChunkHeader(), iPageReader, - true)); + true, + tsFile)); } } else { for (int i = pageReaderList.size() - 1; i >= 0; i--) { @@ -729,7 +742,8 @@ private void unpackOneChunkMetaData(IChunkMetadata chunkMetaData) throws IOExcep chunkMetaData.getVersion(), chunkMetaData.getOffsetOfChunkHeader(), pageReaderList.get(i), - true)); + true, + tsFile)); } } } else { @@ -742,7 +756,8 @@ private void unpackOneChunkMetaData(IChunkMetadata chunkMetaData) throws IOExcep chunkMetaData.getVersion(), chunkMetaData.getOffsetOfChunkHeader(), pageReader, - false))); + false, + tsFile))); } if (LOGGER.isDebugEnabled()) { @@ -2083,6 +2098,7 @@ protected static class VersionPageReader implements IVersionPageReader { protected final boolean isSeq; protected final boolean isAligned; protected final boolean isMem; + protected final File tsFile; VersionPageReader( QueryContext context, @@ -2090,7 +2106,8 @@ protected static class VersionPageReader implements IVersionPageReader { long version, long offset, IPageReader data, - boolean isSeq) { + boolean isSeq, + File tsFile) { this.context = context; this.version = new MergeReaderPriority(fileTimestamp, version, offset, isSeq); this.data = data; @@ -2100,6 +2117,7 @@ protected static class VersionPageReader implements IVersionPageReader { || data instanceof MemAlignedPageReader || data instanceof TablePageReader; this.isMem = data instanceof MemPageReader || data instanceof MemAlignedPageReader; + this.tsFile = tsFile; } @SuppressWarnings("squid:S3740") @@ -2144,6 +2162,21 @@ public TsBlock getAllSatisfiedPageData(boolean ascending) throws IOException { CommonUtils.toString(tsBlock)); } return tsBlock; + } catch (Exception e) { + if (tsFile != null) { + throw new CorruptedTsFileException( + tsFile, + CorruptedTsFileException.Stage.DECODE_PAGE_DATA, + context.isExternalTsFileScan() + ? String.format( + DataNodeQueryMessages + .EXCEPTION_FAILED_TO_DECODE_PAGE_DATA_FROM_TSFILE_ARG_645F5377, + tsFile) + : DataNodeQueryMessages + .EXCEPTION_FAILED_TO_DECODE_PAGE_DATA_THE_TSFILE_MAY_BE_CORRUPTED_PLEASE_CHECK_THE_LOGS_FOR_THE_CORRUPTED_FILE_PATH_54D7C6D9, + e); + } + throw e; } finally { long time = System.nanoTime() - startTime; if (isAligned) { diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/schedule/AbstractDriverThread.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/schedule/AbstractDriverThread.java index 4fb9e8a8cb5ca..7735ecae3e8f7 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/schedule/AbstractDriverThread.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/schedule/AbstractDriverThread.java @@ -23,6 +23,7 @@ import org.apache.iotdb.commons.exception.IoTDBException; import org.apache.iotdb.commons.exception.IoTDBRuntimeException; import org.apache.iotdb.commons.utils.ErrorHandlingCommonUtils; +import org.apache.iotdb.db.exception.CorruptedTsFileException; import org.apache.iotdb.db.i18n.DataNodeQueryMessages; import org.apache.iotdb.db.queryengine.execution.schedule.task.DriverTask; import org.apache.iotdb.db.utils.SetThreadName; @@ -93,6 +94,12 @@ public void run() { next.setAbortCause( new IoTDBRuntimeException( rootCause.getMessage(), DATE_OUT_OF_RANGE.getStatusCode(), true)); + } else if (rootCause instanceof CorruptedTsFileException) { + // CorruptedTsFileException no longer chains the original IOException as its + // cause (it uses addSuppressed instead), so getRootCause returns the exception + // itself and we can match it here. + logger.warn(DataNodeQueryMessages.EXECUTEFAILED, rootCause); + next.setAbortCause(rootCause); } else { logger.warn(DataNodeQueryMessages.EXECUTEFAILED, rootCause); next.setAbortCause( diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/function/tvf/read_tsfile/ExternalTsFileQueryResource.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/function/tvf/read_tsfile/ExternalTsFileQueryResource.java index d16e602e3d23f..9a96145bc8a0b 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/function/tvf/read_tsfile/ExternalTsFileQueryResource.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/relational/function/tvf/read_tsfile/ExternalTsFileQueryResource.java @@ -28,6 +28,7 @@ import org.apache.iotdb.commons.schema.filter.SchemaFilter; import org.apache.iotdb.commons.utils.FileUtils; import org.apache.iotdb.commons.utils.TestOnly; +import org.apache.iotdb.db.exception.CorruptedTsFileException; import org.apache.iotdb.db.i18n.DataNodeQueryMessages; import org.apache.iotdb.db.queryengine.common.MPPQueryContext; import org.apache.iotdb.db.queryengine.common.QueryId; @@ -671,24 +672,45 @@ private class DeviceCollector implements Closeable { private DeviceCollector() { try { for (int fileIndex = 0; fileIndex < tsFilePaths.size(); fileIndex++) { - TsFileSequenceReader reader = - FileReaderManager.getInstance() - .get(tsFilePaths.get(fileIndex), null, true, null, true); - deviceIteratorMap.put(fileIndex, new LazyTsFileDeviceIterator(reader, tableName, null)); + try { + TsFileSequenceReader reader = + FileReaderManager.getInstance() + .get(tsFilePaths.get(fileIndex), null, true, null, true); + deviceIteratorMap.put(fileIndex, new LazyTsFileDeviceIterator(reader, tableName, null)); + } catch (Exception e) { + throw corruptedMetadataIndexNodeException(fileIndex, e); + } } - } catch (IOException e) { + } catch (RuntimeException e) { close(); - throw new RuntimeException( - DataNodeQueryMessages.FAILED_TO_CREATE_EXTERNAL_TSFILE_DEVICE_COLLECTOR, e); + throw e; } } + private CorruptedTsFileException corruptedMetadataIndexNodeException( + int fileIndex, Exception cause) { + File tsFile = sharedTsFileResources.get(fileIndex).getTsFile(); + return new CorruptedTsFileException( + tsFile, + CorruptedTsFileException.Stage.READ_METADATA_INDEX_NODE, + String.format( + DataNodeQueryMessages + .EXCEPTION_FAILED_TO_READ_METADATA_INDEX_NODE_FROM_TSFILE_ARG_EC5B6633, + tsFile), + cause); + } + private boolean hasNextDevice() { - for (LazyTsFileDeviceIterator deviceIterator : deviceIteratorMap.values()) { - if (deviceIterator.hasNext() - || (deviceIterator.hasCurrent() - && !deviceIterator.getCurrentDeviceID().equals(currentDevice))) { - return true; + for (Map.Entry entry : deviceIteratorMap.entrySet()) { + try { + LazyTsFileDeviceIterator deviceIterator = entry.getValue(); + if (deviceIterator.hasNext() + || (deviceIterator.hasCurrent() + && !deviceIterator.getCurrentDeviceID().equals(currentDevice))) { + return true; + } + } catch (Exception e) { + throw corruptedMetadataIndexNodeException(entry.getKey(), e); } } return false; @@ -700,21 +722,25 @@ private IDeviceID nextDevice() { deviceIteratorMap.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry entry = iterator.next(); - LazyTsFileDeviceIterator deviceIterator = entry.getValue(); - IDeviceID currentFileDevice = null; - if (deviceIterator.hasCurrent()) { - currentFileDevice = deviceIterator.getCurrentDeviceID(); - } - if (currentFileDevice == null || currentFileDevice.equals(currentDevice)) { - if (deviceIterator.hasNext()) { - currentFileDevice = deviceIterator.next(); - } else { - iterator.remove(); - continue; + try { + LazyTsFileDeviceIterator deviceIterator = entry.getValue(); + IDeviceID currentFileDevice = null; + if (deviceIterator.hasCurrent()) { + currentFileDevice = deviceIterator.getCurrentDeviceID(); } - } - if (minDevice == null || minDevice.compareTo(currentFileDevice) > 0) { - minDevice = currentFileDevice; + if (currentFileDevice == null || currentFileDevice.equals(currentDevice)) { + if (deviceIterator.hasNext()) { + currentFileDevice = deviceIterator.next(); + } else { + iterator.remove(); + continue; + } + } + if (minDevice == null || minDevice.compareTo(currentFileDevice) > 0) { + minDevice = currentFileDevice; + } + } catch (Exception e) { + throw corruptedMetadataIndexNodeException(entry.getKey(), e); } } currentDevice = minDevice; @@ -725,15 +751,19 @@ private IDeviceID nextDevice() { private void collectCurrentDeviceOffsets() { List deviceOffsets = new ArrayList<>(); for (Map.Entry entry : deviceIteratorMap.entrySet()) { - LazyTsFileDeviceIterator deviceIterator = entry.getValue(); - if (currentDevice != null - && deviceIterator.hasCurrent() - && currentDevice.equals(deviceIterator.getCurrentDeviceID())) { - deviceOffsets.add( - new ExternalTsFileDeviceQueryTask.DeviceOffset( - entry.getKey(), - deviceIterator.getCurrentDeviceMeasurementNodeOffset()[0], - deviceIterator.getCurrentDeviceMeasurementNodeOffset()[1])); + try { + LazyTsFileDeviceIterator deviceIterator = entry.getValue(); + if (currentDevice != null + && deviceIterator.hasCurrent() + && currentDevice.equals(deviceIterator.getCurrentDeviceID())) { + deviceOffsets.add( + new ExternalTsFileDeviceQueryTask.DeviceOffset( + entry.getKey(), + deviceIterator.getCurrentDeviceMeasurementNodeOffset()[0], + deviceIterator.getCurrentDeviceMeasurementNodeOffset()[1])); + } + } catch (Exception e) { + throw corruptedMetadataIndexNodeException(entry.getKey(), e); } } currentDeviceOffsets = deviceOffsets; diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/reader/chunk/DiskAlignedChunkLoader.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/reader/chunk/DiskAlignedChunkLoader.java index 7c1e9d1263518..5af552502857a 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/reader/chunk/DiskAlignedChunkLoader.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/reader/chunk/DiskAlignedChunkLoader.java @@ -37,6 +37,7 @@ import org.apache.tsfile.read.reader.chunk.AlignedChunkReader; import org.apache.tsfile.read.reader.chunk.TableChunkReader; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -142,4 +143,8 @@ public IChunkReader getChunkReader(IChunkMetadata chunkMetaData, Filter globalTi public TsFileID getTsFileID() { return resource.getTsFileID(); } + + public File getTsFile() { + return resource.getTsFile(); + } } diff --git a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/reader/chunk/DiskChunkLoader.java b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/reader/chunk/DiskChunkLoader.java index 7d259a9e50534..eb7abb7ed07f6 100644 --- a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/reader/chunk/DiskChunkLoader.java +++ b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/read/reader/chunk/DiskChunkLoader.java @@ -37,6 +37,7 @@ import org.apache.tsfile.read.reader.IChunkReader; import org.apache.tsfile.read.reader.chunk.ChunkReader; +import java.io.File; import java.io.IOException; import static org.apache.iotdb.db.queryengine.metric.SeriesScanCostMetricSet.INIT_CHUNK_READER_NONALIGNED_DISK; @@ -122,4 +123,8 @@ public IChunkReader getChunkReader(IChunkMetadata chunkMetaData, Filter globalTi public TsFileID getTsFileID() { return resource.getTsFileID(); } + + public File getTsFile() { + return resource.getTsFile(); + } }