Skip to content

Commit b030a75

Browse files
committed
Fix LOAD partition recovery and PREPARE completeness
1 parent d5a2da0 commit b030a75

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
@@ -71,10 +71,12 @@
7171
import java.nio.file.StandardOpenOption;
7272
import java.util.ArrayList;
7373
import java.util.HashMap;
74+
import java.util.HashSet;
7475
import java.util.List;
7576
import java.util.Map;
7677
import java.util.Objects;
7778
import java.util.Optional;
79+
import java.util.Set;
7880
import java.util.concurrent.ConcurrentHashMap;
7981
import java.util.concurrent.atomic.AtomicLong;
8082
import java.util.concurrent.atomic.AtomicReference;
@@ -466,6 +468,37 @@ private void writeDeletion(final DeletionData deletionData) throws IOException {
466468
// Consensus Lifecycle: PREPARE & LOAD
467469
// -------------------------------------------------------------------------
468470

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

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

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

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-
}
510530
// Rebuild from the staged devices so recovered writers also retain the table schemas that
511531
// Pipe needs to enumerate table data in the sealed file.
512532
if (PathUtils.isTableModelDatabase(dataRegion.getDatabaseName())) {
@@ -928,22 +948,28 @@ private static void deleteIfExistsWithRetry(final Path path) throws IOException
928948

929949
/** The name a staged file of this region and time partition has. */
930950
private String stagedFileName(final TTimePartitionSlot timePartitionSlot) {
951+
return stagedFilePrefix() + timePartitionSlot.getStartTime();
952+
}
953+
954+
private String stagedFilePrefix() {
931955
return String.join(
932956
IoTDBConstant.FILE_NAME_SEPARATOR,
933957
dataRegion.getDatabaseName(),
934958
dataRegion.getDataRegionIdString(),
935-
Long.toString(timePartitionSlot.getStartTime()));
959+
"");
936960
}
937961

938962
private static String uuidDirName(final File tsFile) {
939963
final File parent = tsFile.getParentFile();
940964
return parent == null ? tsFile.getName() : parent.getName();
941965
}
942966

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));
967+
private long parseTimePartitionStart(final String tsFileName) {
968+
// The partition may be negative: its sign is part of the number, not a name separator.
969+
return Long.parseLong(
970+
tsFileName.substring(
971+
stagedFilePrefix().length(),
972+
tsFileName.length() - TsFileConstant.TSFILE_SUFFIX.length()));
947973
}
948974

949975
/** 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)