Skip to content

Commit cba841e

Browse files
committed
Fix LOAD partition recovery and PREPARE completeness
1 parent 8a8a6da commit cba841e

2 files changed

Lines changed: 257 additions & 24 deletions

File tree

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/load/TsFileWriterManager.java‎

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,12 @@
7070
import java.nio.file.StandardOpenOption;
7171
import java.util.ArrayList;
7272
import java.util.HashMap;
73+
import java.util.HashSet;
7374
import java.util.List;
7475
import java.util.Map;
7576
import java.util.Objects;
7677
import java.util.Optional;
78+
import java.util.Set;
7779
import java.util.concurrent.ConcurrentHashMap;
7880
import java.util.concurrent.atomic.AtomicLong;
7981
import java.util.concurrent.atomic.AtomicReference;
@@ -465,6 +467,37 @@ private void writeDeletion(final DeletionData deletionData) throws IOException {
465467
// Consensus Lifecycle: PREPARE & LOAD
466468
// -------------------------------------------------------------------------
467469

470+
private void validateStagedFiles(
471+
final Map<TTimePartitionSlot, Long> timePartition2ExpectedDataEnd)
472+
throws IOException, LoadFileException {
473+
final Set<TTimePartitionSlot> partitions = new HashSet<>(timePartition2Writer.keySet());
474+
if (timePartition2ExpectedDataEnd != null) {
475+
partitions.addAll(timePartition2ExpectedDataEnd.keySet());
476+
}
477+
for (final TTimePartitionSlot partition : partitions) {
478+
final TsFilePrecalculatedChunkWriter writer = timePartition2Writer.get(partition);
479+
final File stagedFile =
480+
writer == null
481+
? new File(taskDir, stagedFileName(partition) + TsFileConstant.TSFILE_SUFFIX)
482+
: writer.getFile();
483+
final LoadTsFileProgress progress = timePartition2Progress.get(partition);
484+
final long fileLength = stagedFile.length();
485+
final long expectedDataEnd = expectedDataEndOf(timePartition2ExpectedDataEnd, partition);
486+
if (writer == null
487+
|| progress == null
488+
|| !progress.isReady(fileLength)
489+
|| (expectedDataEnd >= 0L && fileLength < expectedDataEnd)) {
490+
throw new LoadFileException(
491+
String.format(
492+
StorageEngineMessages.EXCEPTION_LOAD_CONSENSUS_STAGED_FILE_NOT_CONTINUOUS_F9408C19,
493+
stagedFile.getAbsolutePath(),
494+
uuidDirName(stagedFile),
495+
Math.max(progress == null ? 0L : progress.getTotalLength(), expectedDataEnd),
496+
fileLength));
497+
}
498+
}
499+
}
500+
468501
/**
469502
* Seals modification files, validates chunk continuity, and seals TsFiles in the PREPARE round.
470503
*/
@@ -476,36 +509,23 @@ void prepare(
476509
checkNotClosed();
477510
touch();
478511

479-
// 1. Close modification files first to freeze deletion views
512+
// Validate every announced partition before sealing any file. A missing partition or a file
513+
// without durable chunk records must not turn into a successful partial import.
514+
validateStagedFiles(timePartition2ExpectedDataEnd);
515+
516+
// Close modification files to freeze deletion views.
480517
for (final ModificationFile modFile : timePartition2ModificationFile.values()) {
481518
modFile.close();
482519
}
483520

484-
// 2. Validate continuity and seal writer metadata zone
521+
// Seal writer metadata zones only after every partition has passed validation.
485522
for (final Map.Entry<TTimePartitionSlot, TsFilePrecalculatedChunkWriter> entry :
486523
timePartition2Writer.entrySet()) {
487524
final TsFilePrecalculatedChunkWriter writer = entry.getValue();
488525
if (writer.isSealed()) {
489526
continue;
490527
}
491528

492-
final LoadTsFileProgress progress = timePartition2Progress.get(entry.getKey());
493-
if (progress != null && progress.exists() && progress.getTotalLength() > 0) {
494-
final long fileLength = writer.getFile().length();
495-
final long expectedDataEnd =
496-
expectedDataEndOf(timePartition2ExpectedDataEnd, entry.getKey());
497-
if (!progress.isReady(fileLength)
498-
|| (expectedDataEnd >= 0L && fileLength < expectedDataEnd)) {
499-
throw new LoadFileException(
500-
String.format(
501-
StorageEngineMessages
502-
.EXCEPTION_LOAD_CONSENSUS_STAGED_FILE_NOT_CONTINUOUS_F9408C19,
503-
writer.getFile().getAbsolutePath(),
504-
uuidDirName(writer.getFile()),
505-
Math.max(progress.getTotalLength(), expectedDataEnd),
506-
fileLength));
507-
}
508-
}
509529
// Rebuild from the staged devices so recovered writers also retain the table schemas that
510530
// Pipe needs to enumerate table data in the sealed file.
511531
if (PathUtils.isTableModelDatabase(dataRegion.getDatabaseName())) {
@@ -919,22 +939,28 @@ private static void deleteIfExistsWithRetry(final Path path) throws IOException
919939

920940
/** The name a staged file of this region and time partition has. */
921941
private String stagedFileName(final TTimePartitionSlot timePartitionSlot) {
942+
return stagedFilePrefix() + timePartitionSlot.getStartTime();
943+
}
944+
945+
private String stagedFilePrefix() {
922946
return String.join(
923947
IoTDBConstant.FILE_NAME_SEPARATOR,
924948
dataRegion.getDatabaseName(),
925949
dataRegion.getDataRegionIdString(),
926-
Long.toString(timePartitionSlot.getStartTime()));
950+
"");
927951
}
928952

929953
private static String uuidDirName(final File tsFile) {
930954
final File parent = tsFile.getParentFile();
931955
return parent == null ? tsFile.getName() : parent.getName();
932956
}
933957

934-
private static long parseTimePartitionStart(final String tsFileName) {
935-
final String baseName = tsFileName.substring(0, tsFileName.lastIndexOf('.'));
936-
final int lastDash = baseName.lastIndexOf(IoTDBConstant.FILE_NAME_SEPARATOR);
937-
return Long.parseLong(baseName.substring(lastDash + 1));
958+
private long parseTimePartitionStart(final String tsFileName) {
959+
// The partition may be negative: its sign is part of the number, not a name separator.
960+
return Long.parseLong(
961+
tsFileName.substring(
962+
stagedFilePrefix().length(),
963+
tsFileName.length() - TsFileConstant.TSFILE_SUFFIX.length()));
938964
}
939965

940966
/** The bytes this piece added to each staged file, as the refs a replica reads them back with. */
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.iotdb.db.storageengine.load;
21+
22+
import org.apache.iotdb.common.rpc.thrift.TTimePartitionSlot;
23+
import org.apache.iotdb.db.exception.load.LoadFileException;
24+
import org.apache.iotdb.db.queryengine.plan.scheduler.load.ChunkOffsetCalculator;
25+
import org.apache.iotdb.db.storageengine.dataregion.DataRegion;
26+
import org.apache.iotdb.db.storageengine.load.splitter.NonAlignedChunkData;
27+
28+
import org.apache.tsfile.enums.TSDataType;
29+
import org.apache.tsfile.file.header.ChunkHeader;
30+
import org.apache.tsfile.file.metadata.IDeviceID;
31+
import org.apache.tsfile.file.metadata.StringArrayDeviceID;
32+
import org.apache.tsfile.file.metadata.enums.CompressionType;
33+
import org.apache.tsfile.file.metadata.enums.TSEncoding;
34+
import org.apache.tsfile.read.TsFileReader;
35+
import org.apache.tsfile.read.common.Path;
36+
import org.apache.tsfile.read.common.RowRecord;
37+
import org.apache.tsfile.read.expression.QueryExpression;
38+
import org.apache.tsfile.read.query.dataset.QueryDataSet;
39+
import org.junit.Before;
40+
import org.junit.Rule;
41+
import org.junit.Test;
42+
import org.junit.rules.TemporaryFolder;
43+
import org.mockito.Mockito;
44+
45+
import java.io.File;
46+
import java.io.IOException;
47+
import java.util.Collections;
48+
import java.util.List;
49+
import java.util.Map;
50+
51+
import static org.junit.Assert.assertEquals;
52+
import static org.junit.Assert.assertFalse;
53+
import static org.junit.Assert.assertTrue;
54+
import static org.junit.Assert.fail;
55+
56+
public class TsFileWriterManagerRecoveryTest {
57+
58+
private static final String DATABASE = "root.load_recovery_test";
59+
private static final IDeviceID DEVICE =
60+
new StringArrayDeviceID("root", "load_recovery_test", "d1");
61+
62+
@Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder();
63+
64+
private DataRegion dataRegion;
65+
66+
@Before
67+
public void setUp() {
68+
dataRegion = Mockito.mock(DataRegion.class);
69+
Mockito.when(dataRegion.getDatabaseName()).thenReturn(DATABASE);
70+
Mockito.when(dataRegion.getDataRegionIdString()).thenReturn("0");
71+
}
72+
73+
@Test
74+
public void testResumeNegativeTimePartition() throws Exception {
75+
assertTimePartitionResumes(-604_800_000L);
76+
}
77+
78+
@Test
79+
public void testResumeMinimumTimePartition() throws Exception {
80+
assertTimePartitionResumes(Long.MIN_VALUE);
81+
}
82+
83+
private void assertTimePartitionResumes(final long partitionStart) throws Exception {
84+
final File taskDir = temporaryFolder.newFolder("task");
85+
final ChunkOffsetCalculator calculator = new ChunkOffsetCalculator();
86+
final NonAlignedChunkData first = chunk(partitionStart, 1);
87+
final NonAlignedChunkData second = chunk(partitionStart, 2);
88+
calculator.assign(first);
89+
calculator.assign(second);
90+
91+
TsFileWriterManager manager = new TsFileWriterManager(dataRegion, taskDir);
92+
try {
93+
manager.writePiece(List.of(first));
94+
manager.close(true);
95+
manager = new TsFileWriterManager(dataRegion, taskDir).recoverFromDisk();
96+
manager.writePiece(List.of(second));
97+
manager.prepare(
98+
false, Collections.emptyMap(), Map.of(second.getTimePartitionSlot(), chunkEnd(second)));
99+
100+
// Both the chunk staged before recovery and the one appended afterwards must survive.
101+
assertPoints(stagedFile(taskDir, partitionStart), partitionStart, 1, 2);
102+
} finally {
103+
manager.close();
104+
}
105+
}
106+
107+
@Test
108+
public void testPrepareRejectsMissingPartition() throws Exception {
109+
final File taskDir = temporaryFolder.newFolder("task");
110+
final TsFileWriterManager manager = new TsFileWriterManager(dataRegion, taskDir);
111+
final NonAlignedChunkData first = chunk(0L, 1);
112+
final NonAlignedChunkData missing = chunk(604_800_000L, 1);
113+
new ChunkOffsetCalculator().assign(first);
114+
new ChunkOffsetCalculator().assign(missing);
115+
final Map<TTimePartitionSlot, Long> expectedDataEnd =
116+
Map.of(
117+
first.getTimePartitionSlot(), chunkEnd(first),
118+
missing.getTimePartitionSlot(), chunkEnd(missing));
119+
try {
120+
manager.writePiece(List.of(first));
121+
final long unsealedLength = stagedFile(taskDir, 0L).length();
122+
assertPrepareFails(manager, expectedDataEnd, stagedFile(taskDir, 604_800_000L));
123+
assertEquals(unsealedLength, stagedFile(taskDir, 0L).length());
124+
125+
manager.writePiece(List.of(missing));
126+
manager.prepare(false, Collections.emptyMap(), expectedDataEnd);
127+
assertPoints(stagedFile(taskDir, 0L), 0L, 1);
128+
assertPoints(stagedFile(taskDir, 604_800_000L), 604_800_000L, 1);
129+
} finally {
130+
manager.close();
131+
}
132+
}
133+
134+
@Test
135+
public void testPrepareRejectsPartitionWithoutRecordedChunks() throws Exception {
136+
final File taskDir = temporaryFolder.newFolder("task");
137+
final TsFileWriterManager manager = new TsFileWriterManager(dataRegion, taskDir);
138+
final NonAlignedChunkData complete = chunk(0L, 1);
139+
new ChunkOffsetCalculator().assign(complete);
140+
final Map<TTimePartitionSlot, Long> expectedDataEnd =
141+
Map.of(complete.getTimePartitionSlot(), chunkEnd(complete));
142+
try {
143+
// A failed piece may have opened the file without recording any durable chunk.
144+
try {
145+
manager.writePiece(List.of(chunk(0L, 1)));
146+
fail("A chunk without a layout must fail after opening its staged file");
147+
} catch (final IOException expected) {
148+
assertTrue(stagedFile(taskDir, 0L).isFile());
149+
}
150+
assertPrepareFails(manager, expectedDataEnd, stagedFile(taskDir, 0L));
151+
152+
manager.writePiece(List.of(complete));
153+
manager.prepare(false, Collections.emptyMap(), expectedDataEnd);
154+
assertPoints(stagedFile(taskDir, 0L), 0L, 1);
155+
} finally {
156+
manager.close();
157+
}
158+
}
159+
160+
private static void assertPrepareFails(
161+
final TsFileWriterManager manager,
162+
final Map<TTimePartitionSlot, Long> expectedDataEnd,
163+
final File missingDataFile)
164+
throws Exception {
165+
try {
166+
manager.prepare(false, Collections.emptyMap(), expectedDataEnd);
167+
fail("PREPARE must reject a partition whose announced data has not been staged");
168+
} catch (final LoadFileException e) {
169+
assertTrue(e.getMessage(), e.getMessage().contains(missingDataFile.getAbsolutePath()));
170+
}
171+
}
172+
173+
private static NonAlignedChunkData chunk(final long partitionStart, final int value) {
174+
final NonAlignedChunkData chunk =
175+
new NonAlignedChunkData(
176+
DEVICE,
177+
new ChunkHeader(
178+
"s1", 0, TSDataType.INT32, CompressionType.UNCOMPRESSED, TSEncoding.PLAIN, 0),
179+
new TTimePartitionSlot(partitionStart));
180+
chunk.writeDecodePage(new long[] {partitionStart + value}, new Object[] {value}, 1);
181+
chunk.endChunk();
182+
return chunk;
183+
}
184+
185+
private static long chunkEnd(final NonAlignedChunkData chunk) {
186+
return chunk.getChunkLayout().offset() + chunk.getChunkLayout().length();
187+
}
188+
189+
private static File stagedFile(final File taskDir, final long partitionStart) {
190+
return new File(taskDir, DATABASE + "-0-" + partitionStart + ".tsfile");
191+
}
192+
193+
private static void assertPoints(final File file, final long partitionStart, final int... values)
194+
throws Exception {
195+
try (final TsFileReader reader = new TsFileReader(file)) {
196+
final QueryDataSet result =
197+
reader.query(QueryExpression.create(List.of(new Path(DEVICE, "s1", false)), null));
198+
for (final int value : values) {
199+
assertTrue(result.hasNext());
200+
final RowRecord row = result.next();
201+
assertEquals(partitionStart + value, row.getTimestamp());
202+
assertEquals(value, row.getField(0).getIntV());
203+
}
204+
assertFalse(result.hasNext());
205+
}
206+
}
207+
}

0 commit comments

Comments
 (0)