Skip to content

Commit 34cc346

Browse files
authored
Fix query reuse of flushing memtable TVList (#17718)
1 parent b8cf54b commit 34cc346

9 files changed

Lines changed: 122 additions & 14 deletions

File tree

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/QueryContext.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,8 @@ public void setIgnoreAllNullRows(boolean ignoreAllNullRows) {
267267
this.ignoreAllNullRows = ignoreAllNullRows;
268268
}
269269

270-
public void addTVListToSet(Map<TVList, Integer> tvListMap) {
271-
tvListSet.addAll(tvListMap.keySet());
270+
public void addTVListToSet(Set<TVList> set) {
271+
tvListSet.addAll(set);
272272
}
273273

274274
public void addRowLevelFilteredCount(long count) {

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/schemaengine/schemaregion/utils/ResourceByPathUtils.java‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,46 @@ protected Map<TVList, Integer> prepareTvListMapForQuery(
166166
}
167167

168168
if (!isWorkMemTable) {
169+
/*
170+
* 1. Q1 queries this TVList while it is still in the working memtable and records a smaller
171+
* visible row count.
172+
* 2. Later writes append out-of-order rows to the same TVList, then FLUSH moves the
173+
* memtable to the flushing list.
174+
* 3. Q2 queries the flushing memtable. If Q2 directly reuses the original mutable TVList,
175+
* Q2's query-side sort may reorder the indices in place.
176+
* 4. Q1 continues to read with its old row count and the reordered indices. The converted
177+
* value index can exceed Q1's bitmap range and cause out-of-bound access.
178+
*
179+
* Therefore, this flushing branch can reuse the original list only when it is already
180+
* sorted or no active query is using it. Otherwise, Q2 should read from
181+
* workingListForFlush.
182+
*/
183+
boolean canUseListDirectly = list.isSorted() || list.getQueryContextSet().isEmpty();
169184
LOGGER.debug(
170185
"Flushing MemTable - add current query context to mutable TVList's query list");
171-
list.getQueryContextSet().add(context);
172-
tvListQueryMap.put(list, list.rowCount());
186+
if (canUseListDirectly) {
187+
list.getQueryContextSet().add(context);
188+
tvListQueryMap.put(list, list.rowCount());
189+
} else {
190+
TVList workingListForFlushSort = memChunk.initWorkingListForFlushIfNecessary(list, true);
191+
/*
192+
* The query will read from workingListForFlushSort, but cloneForFlushSort() only clones
193+
* times and indices. The value arrays and bitmaps are still shared with the original
194+
* list.
195+
*
196+
* Therefore, this query must also hold the original list until it finishes. Adding
197+
* context to list.getQueryContextSet() lets flush/query cleanup see that the original
198+
* list is still in use. Adding list to context.tvListSet makes
199+
* releaseTVListOwnedByQuery() remove this context from the original list later.
200+
*
201+
* Do not put the original list into tvListQueryMap here. The actual read path must use
202+
* workingListForFlushSort to avoid sorting the original list in place.
203+
*/
204+
list.getQueryContextSet().add(context);
205+
context.addTVListToSet(Collections.singleton(list));
206+
workingListForFlushSort.getQueryContextSet().add(context);
207+
tvListQueryMap.put(workingListForFlushSort, workingListForFlushSort.rowCount());
208+
}
173209
} else {
174210
if (list.isSorted() || list.getQueryContextSet().isEmpty()) {
175211
LOGGER.debug(

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/flush/MemTableFlushTask.java‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,6 @@ public void run() {
273273
times = new long[MAX_NUMBER_OF_POINTS_IN_PAGE];
274274
}
275275
writableMemChunk.encode(ioTaskQueue, encodeInfo, times);
276-
writableMemChunk.releaseTemporaryTvListForFlush();
277276
long subTaskTime = System.currentTimeMillis() - starTime;
278277
WRITING_METRICS.recordFlushSubTaskCost(WritingMetrics.ENCODING_TASK, subTaskTime);
279278
memSerializeTime += subTaskTime;

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AbstractWritableMemChunk.java‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,9 @@ public void sortTvListForFlush() {
210210
/*
211211
* Concurrency background:
212212
*
213-
* A query may start earlier and record the current row count (rows) of the TVList as its visible range.
214-
* After that, new unseq writes may arrive and immediately trigger a flush, which will sort the TVList.
213+
* A query may start earlier and record the current row count (rows) of the TVList as its
214+
* visible range. After that, new unseq writes may arrive and immediately trigger a flush, which
215+
* will sort the TVList.
215216
*
216217
* During sorting, the underlying indices array of the TVList may be reordered.
217218
* If the query continues to use the previously recorded rows as its upper bound,
@@ -223,6 +224,9 @@ public void sortTvListForFlush() {
223224
* To avoid this issue, when there are active queries on the working TVList, we must
224225
* clone the times and indices before sorting, so that the flush sort does not mutate
225226
* the data structures that concurrent queries rely on.
227+
*
228+
* Flushing-memtable queries may also reuse workingListForFlush instead of the original working
229+
* TVList for the same reason.
226230
*/
227231
boolean needCloneTimesAndIndicesInWorkingTVList;
228232
workingList.lockQueryList();
@@ -232,7 +236,7 @@ public void sortTvListForFlush() {
232236
workingList.unlockQueryList();
233237
}
234238
workingListForFlush =
235-
needCloneTimesAndIndicesInWorkingTVList ? workingList.cloneForFlushSort() : workingList;
239+
initWorkingListForFlushIfNecessary(workingList, needCloneTimesAndIndicesInWorkingTVList);
236240
workingListForFlush.sort();
237241
}
238242

@@ -274,4 +278,13 @@ public abstract void encode(
274278

275279
@Override
276280
public abstract void setEncryptParameter(EncryptParameter encryptParameter);
281+
282+
public synchronized TVList initWorkingListForFlushIfNecessary(
283+
TVList workingList, boolean needCloneTimesAndIndicesInWorkingTVList) {
284+
if (workingListForFlush == null) {
285+
workingListForFlush =
286+
needCloneTimesAndIndicesInWorkingTVList ? workingList.cloneForFlushSort() : workingList;
287+
}
288+
return workingListForFlush;
289+
}
277290
}

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/AlignedReadOnlyMemChunk.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public AlignedReadOnlyMemChunk(
109109
this.valueStatisticsList = new ArrayList<>();
110110
this.alignedTvListQueryMap = alignedTvListQueryMap;
111111
this.columnIndexList = columnIndexList;
112-
this.context.addTVListToSet(alignedTvListQueryMap);
112+
this.context.addTVListToSet(alignedTvListQueryMap.keySet());
113113
}
114114

115115
@Override

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/IWritableMemChunk.java‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,7 @@ default long getMinTime() {
132132
void setWorkingTVList(TVList list);
133133

134134
void setEncryptParameter(EncryptParameter encryptParameter);
135+
136+
TVList initWorkingListForFlushIfNecessary(
137+
TVList workingList, boolean needCloneTimesAndIndicesInWorkingTVList);
135138
}

‎iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/ReadOnlyMemChunk.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public ReadOnlyMemChunk(
128128
this.deletionList = deletionList;
129129
this.tvListQueryMap = tvListQueryMap;
130130
this.pageStatisticsList = new ArrayList<>();
131-
this.context.addTVListToSet(tvListQueryMap);
131+
this.context.addTVListToSet(tvListQueryMap.keySet());
132132
}
133133

134134
public void sortTvLists() {

‎iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceExecutionTest.java‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
import org.apache.iotdb.db.utils.datastructure.AlignedTVList;
4545
import org.apache.iotdb.db.utils.datastructure.TVList;
4646

47-
import com.google.common.collect.ImmutableMap;
47+
import com.google.common.collect.ImmutableSet;
4848
import org.apache.tsfile.enums.TSDataType;
4949
import org.apache.tsfile.file.metadata.IDeviceID;
5050
import org.apache.tsfile.file.metadata.enums.CompressionType;
@@ -145,13 +145,13 @@ public void testTVListOwnerTransfer() throws InterruptedException {
145145
FragmentInstanceExecution execution1 =
146146
createFragmentInstanceExecution(1, instanceNotificationExecutor);
147147
FragmentInstanceContext fragmentInstanceContext1 = execution1.getFragmentInstanceContext();
148-
fragmentInstanceContext1.addTVListToSet(ImmutableMap.of(tvList, 0));
148+
fragmentInstanceContext1.addTVListToSet(ImmutableSet.of(tvList));
149149
tvList.getQueryContextSet().add(fragmentInstanceContext1);
150150

151151
FragmentInstanceExecution execution2 =
152152
createFragmentInstanceExecution(2, instanceNotificationExecutor);
153153
FragmentInstanceContext fragmentInstanceContext2 = execution2.getFragmentInstanceContext();
154-
fragmentInstanceContext2.addTVListToSet(ImmutableMap.of(tvList, 0));
154+
fragmentInstanceContext2.addTVListToSet(ImmutableSet.of(tvList));
155155
tvList.getQueryContextSet().add(fragmentInstanceContext2);
156156

157157
// mock flush's behavior

‎iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/PrimitiveMemTableTest.java‎

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,63 @@ public void testWriteAndFlushSortDuringQuerySortTVListAndActualQueryExecution()
259259
}
260260
}
261261

262+
@Test
263+
public void testFlushingQueryDoesNotSortWorkingTVListUsedByPreviousQuery()
264+
throws QueryProcessException, IOException, IllegalPathException {
265+
266+
PrimitiveMemTable memTable = new PrimitiveMemTable("root.test", "0");
267+
List<IMeasurementSchema> measurementSchemas =
268+
Arrays.asList(
269+
new MeasurementSchema("s1", TSDataType.INT32),
270+
new MeasurementSchema("s2", TSDataType.INT32),
271+
new MeasurementSchema("s3", TSDataType.INT32));
272+
StringArrayDeviceID deviceID = new StringArrayDeviceID("root.test.d1");
273+
for (int i = 1000; i < 2000; i++) {
274+
memTable.writeAlignedRow(deviceID, measurementSchemas, i, new Object[] {i, i, i});
275+
}
276+
277+
ResourceByPathUtils resourcesByPathUtils =
278+
ResourceByPathUtils.getResourceInstance(
279+
new AlignedFullPath(deviceID, Arrays.asList("s1", "s2", "s3"), measurementSchemas));
280+
AlignedReadOnlyMemChunk firstQueryMemChunk =
281+
(AlignedReadOnlyMemChunk)
282+
resourcesByPathUtils.getReadOnlyMemChunkFromMemTable(
283+
new QueryContext(1, false), memTable, null, Long.MAX_VALUE, null);
284+
TVList originalWorkingList = memTable.getWritableMemChunk(deviceID, "").getWorkingTVList();
285+
Assert.assertSame(
286+
originalWorkingList,
287+
firstQueryMemChunk.getAligendTvListQueryMap().keySet().iterator().next());
288+
289+
for (int i = 1; i <= 50; i++) {
290+
memTable.writeAlignedRow(deviceID, measurementSchemas, i, new Object[] {i, i, i});
291+
}
292+
memTable.delete(
293+
new TreeDeletionEntry(new MeasurementPath("root.test.d1.s1", TSDataType.INT32), 1, 10));
294+
memTable.delete(
295+
new TreeDeletionEntry(new MeasurementPath("root.test.d1.s2", TSDataType.INT32), 1, 10));
296+
memTable.delete(
297+
new TreeDeletionEntry(new MeasurementPath("root.test.d1.s3", TSDataType.INT32), 1, 10));
298+
Assert.assertFalse(originalWorkingList.isSorted());
299+
300+
AlignedReadOnlyMemChunk flushingQueryMemChunk =
301+
(AlignedReadOnlyMemChunk)
302+
resourcesByPathUtils.getReadOnlyMemChunkFromMemTable(
303+
new QueryContext(2, false), memTable, new ArrayList<>(), Long.MAX_VALUE, null);
304+
TVList flushingQueryList =
305+
flushingQueryMemChunk.getAligendTvListQueryMap().keySet().iterator().next();
306+
Assert.assertNotSame(originalWorkingList, flushingQueryList);
307+
308+
flushingQueryMemChunk.sortTvLists();
309+
Assert.assertFalse(originalWorkingList.isSorted());
310+
311+
firstQueryMemChunk.sortTvLists();
312+
MemPointIterator memPointIterator =
313+
firstQueryMemChunk.createMemPointIterator(Ordering.ASC, null);
314+
while (memPointIterator.hasNextBatch()) {
315+
memPointIterator.nextBatch();
316+
}
317+
}
318+
262319
@Test
263320
public void memSeriesToStringTest() throws IOException {
264321
TSDataType dataType = TSDataType.INT32;
@@ -767,7 +824,7 @@ public void testReleaseWithNotEnoughMemory() throws CpuNotEnoughException {
767824
list.getQueryContextSet().add(queryContext);
768825
Map<TVList, Integer> tvlistMap = new HashMap<>();
769826
tvlistMap.put(list, 100);
770-
queryContext.addTVListToSet(tvlistMap);
827+
queryContext.addTVListToSet(tvlistMap.keySet());
771828

772829
// fragment instance execution
773830
IDriverScheduler scheduler = Mockito.mock(IDriverScheduler.class);

0 commit comments

Comments
 (0)