4545import org .apache .iotdb .db .storageengine .load .splitter .DeletionData ;
4646import org .apache .iotdb .db .storageengine .load .splitter .TsFileData ;
4747
48+ import org .apache .tsfile .common .conf .TSFileConfig ;
4849import org .apache .tsfile .common .constant .TsFileConstant ;
4950import org .apache .tsfile .enums .TSDataType ;
5051import org .apache .tsfile .exception .write .PageException ;
5455import org .apache .tsfile .file .metadata .IDeviceID ;
5556import org .apache .tsfile .file .metadata .statistics .Statistics ;
5657import org .apache .tsfile .read .TimeValuePair ;
58+ import org .apache .tsfile .read .TsFileSequenceReader ;
5759import org .apache .tsfile .read .common .Chunk ;
5860import org .apache .tsfile .utils .Pair ;
5961import org .apache .tsfile .utils .RamUsageEstimator ;
7173import java .nio .file .StandardOpenOption ;
7274import java .util .ArrayList ;
7375import java .util .HashMap ;
76+ import java .util .HashSet ;
7477import java .util .List ;
7578import java .util .Map ;
7679import java .util .Objects ;
7780import java .util .Optional ;
81+ import java .util .Set ;
7882import java .util .concurrent .ConcurrentHashMap ;
7983import java .util .concurrent .atomic .AtomicLong ;
8084import java .util .concurrent .atomic .AtomicReference ;
@@ -466,6 +470,37 @@ private void writeDeletion(final DeletionData deletionData) throws IOException {
466470 // Consensus Lifecycle: PREPARE & LOAD
467471 // -------------------------------------------------------------------------
468472
473+ private void validateStagedFiles (
474+ final Map <TTimePartitionSlot , Long > timePartition2ExpectedDataEnd )
475+ throws IOException , LoadFileException {
476+ final Set <TTimePartitionSlot > partitions = new HashSet <>(timePartition2Writer .keySet ());
477+ if (timePartition2ExpectedDataEnd != null ) {
478+ partitions .addAll (timePartition2ExpectedDataEnd .keySet ());
479+ }
480+ for (final TTimePartitionSlot partition : partitions ) {
481+ final TsFilePrecalculatedChunkWriter writer = timePartition2Writer .get (partition );
482+ final File stagedFile =
483+ writer == null
484+ ? new File (taskDir , stagedFileName (partition ) + TsFileConstant .TSFILE_SUFFIX )
485+ : writer .getFile ();
486+ final LoadTsFileProgress progress = timePartition2Progress .get (partition );
487+ final long fileLength = stagedFile .length ();
488+ final long expectedDataEnd = expectedDataEndOf (timePartition2ExpectedDataEnd , partition );
489+ if (writer == null
490+ || progress == null
491+ || !progress .isReady (fileLength )
492+ || (expectedDataEnd >= 0L && fileLength < expectedDataEnd )) {
493+ throw new LoadFileException (
494+ String .format (
495+ StorageEngineMessages .EXCEPTION_LOAD_CONSENSUS_STAGED_FILE_NOT_CONTINUOUS_F9408C19 ,
496+ stagedFile .getAbsolutePath (),
497+ uuidDirName (stagedFile ),
498+ Math .max (progress == null ? 0L : progress .getTotalLength (), expectedDataEnd ),
499+ fileLength ));
500+ }
501+ }
502+ }
503+
469504 /**
470505 * Seals modification files, validates chunk continuity, and seals TsFiles in the PREPARE round.
471506 */
@@ -477,36 +512,23 @@ void prepare(
477512 checkNotClosed ();
478513 touch ();
479514
480- // 1. Close modification files first to freeze deletion views
515+ // Validate every announced partition before sealing any file. A missing partition or a file
516+ // without durable chunk records must not turn into a successful partial import.
517+ validateStagedFiles (timePartition2ExpectedDataEnd );
518+
519+ // Close modification files to freeze deletion views.
481520 for (final ModificationFile modFile : timePartition2ModificationFile .values ()) {
482521 modFile .close ();
483522 }
484523
485- // 2. Validate continuity and seal writer metadata zone
524+ // Seal writer metadata zones only after every partition has passed validation.
486525 for (final Map .Entry <TTimePartitionSlot , TsFilePrecalculatedChunkWriter > entry :
487526 timePartition2Writer .entrySet ()) {
488527 final TsFilePrecalculatedChunkWriter writer = entry .getValue ();
489528 if (writer .isSealed ()) {
490529 continue ;
491530 }
492531
493- final LoadTsFileProgress progress = timePartition2Progress .get (entry .getKey ());
494- if (progress != null && progress .exists () && progress .getTotalLength () > 0 ) {
495- final long fileLength = writer .getFile ().length ();
496- final long expectedDataEnd =
497- expectedDataEndOf (timePartition2ExpectedDataEnd , entry .getKey ());
498- if (!progress .isReady (fileLength )
499- || (expectedDataEnd >= 0L && fileLength < expectedDataEnd )) {
500- throw new LoadFileException (
501- String .format (
502- StorageEngineMessages
503- .EXCEPTION_LOAD_CONSENSUS_STAGED_FILE_NOT_CONTINUOUS_F9408C19 ,
504- writer .getFile ().getAbsolutePath (),
505- uuidDirName (writer .getFile ()),
506- Math .max (progress .getTotalLength (), expectedDataEnd ),
507- fileLength ));
508- }
509- }
510532 // Rebuild from the staged devices so recovered writers also retain the table schemas that
511533 // Pipe needs to enumerate table data in the sealed file.
512534 if (PathUtils .isTableModelDatabase (dataRegion .getDatabaseName ())) {
@@ -721,7 +743,21 @@ TsFileWriterManager recoverFromDisk() throws IOException {
721743 final TTimePartitionSlot timePartitionSlot =
722744 new TTimePartitionSlot (parseTimePartitionStart (tsFile .getName ()));
723745 final long resumeOffset = progress .getTotalLength ();
724- if (tsFile .length () > resumeOffset ) {
746+ final TsFileResource resource = new TsFileResource (tsFile );
747+ boolean sealed = false ;
748+ if (resource .resourceFileExists ()) {
749+ try (final TsFileSequenceReader reader =
750+ new TsFileSequenceReader (tsFile .getAbsolutePath (), false )) {
751+ sealed = TSFileConfig .MAGIC_STRING .equals (reader .readTailMagic ());
752+ }
753+ if (sealed ) {
754+ resource .deserialize ();
755+ resource .setStatus (TsFileResourceStatus .NORMAL );
756+ }
757+ }
758+ // A resource sidecar and a completed footer record a successful PREPARE. Its coordinator
759+ // may send COMMIT immediately after recovery, so preserve both the sealed bytes and marks.
760+ if (!sealed && tsFile .length () > resumeOffset ) {
725761 try (final FileChannel truncatingChannel =
726762 FileChannel .open (tsFile .toPath (), StandardOpenOption .READ , StandardOpenOption .WRITE )) {
727763 truncatingChannel .truncate (resumeOffset );
@@ -745,15 +781,15 @@ TsFileWriterManager recoverFromDisk() throws IOException {
745781 try {
746782 channel =
747783 FileChannel .open (tsFile .toPath (), StandardOpenOption .READ , StandardOpenOption .WRITE );
748- channel .position (resumeOffset );
784+ channel .position (sealed ? tsFile . length () : resumeOffset );
749785 final TsFilePrecalculatedChunkWriter writer =
750- new TsFilePrecalculatedChunkWriter (tsFile , channel );
786+ new TsFilePrecalculatedChunkWriter (tsFile , channel , sealed );
751787 writer .setRejectHoles (LoadConsensusCompat .usesIoTConsensusV2 ());
752788 writer .restoreChunkMetadata (restoreChunkMetadata (records ));
753789 publishedLengths .put (tsFile , tsFile .length ());
754790
755791 timePartition2Writer .put (timePartitionSlot , writer );
756- timePartition2Resource .put (timePartitionSlot , new TsFileResource ( tsFile ) );
792+ timePartition2Resource .put (timePartitionSlot , resource );
757793 timePartition2Progress .put (timePartitionSlot , progress );
758794
759795 final File modificationFile = ModificationFile .getExclusiveMods (tsFile );
@@ -928,22 +964,28 @@ private static void deleteIfExistsWithRetry(final Path path) throws IOException
928964
929965 /** The name a staged file of this region and time partition has. */
930966 private String stagedFileName (final TTimePartitionSlot timePartitionSlot ) {
967+ return stagedFilePrefix () + timePartitionSlot .getStartTime ();
968+ }
969+
970+ private String stagedFilePrefix () {
931971 return String .join (
932972 IoTDBConstant .FILE_NAME_SEPARATOR ,
933973 dataRegion .getDatabaseName (),
934974 dataRegion .getDataRegionIdString (),
935- Long . toString ( timePartitionSlot . getStartTime ()) );
975+ "" );
936976 }
937977
938978 private static String uuidDirName (final File tsFile ) {
939979 final File parent = tsFile .getParentFile ();
940980 return parent == null ? tsFile .getName () : parent .getName ();
941981 }
942982
943- private static long parseTimePartitionStart (final String tsFileName ) {
944- final String baseName = tsFileName .substring (0 , tsFileName .lastIndexOf ('.' ));
945- final int lastDash = baseName .lastIndexOf (IoTDBConstant .FILE_NAME_SEPARATOR );
946- return Long .parseLong (baseName .substring (lastDash + 1 ));
983+ private long parseTimePartitionStart (final String tsFileName ) {
984+ // The partition may be negative: its sign is part of the number, not a name separator.
985+ return Long .parseLong (
986+ tsFileName .substring (
987+ stagedFilePrefix ().length (),
988+ tsFileName .length () - TsFileConstant .TSFILE_SUFFIX .length ()));
947989 }
948990
949991 /** The bytes this piece added to each staged file, as the refs a replica reads them back with. */
0 commit comments