This is an automated email from the ASF dual-hosted git repository.
jt2594838 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iotdb.git
The following commit(s) were added to refs/heads/master by this push:
new abcbf2ed525 [Subscription] Release exhausted TsFile parsers before
batch ACK (#18820)
abcbf2ed525 is described below
commit abcbf2ed525f66b7609e390139a90cea308e3595
Author: Caideyipi <[email protected]>
AuthorDate: Sat Oct 10 19:42:33 2026 +0800
[Subscription] Release exhausted TsFile parsers before batch ACK (#18820)
---
.../batch/SubscriptionPipeTabletEventBatch.java | 21 +-
.../SubscriptionPipeTabletEventBatchTest.java | 256 +++++++++++++++++++++
2 files changed, 271 insertions(+), 6 deletions(-)
diff --git
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/event/batch/SubscriptionPipeTabletEventBatch.java
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/event/batch/SubscriptionPipeTabletEventBatch.java
index 67321b04651..1d3987a6dd2 100644
---
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/event/batch/SubscriptionPipeTabletEventBatch.java
+++
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/subscription/event/batch/SubscriptionPipeTabletEventBatch.java
@@ -405,9 +405,7 @@ public class SubscriptionPipeTabletEventBatch extends
SubscriptionPipeEventBatch
if (currentTabletInsertionEventsIterator.hasNext()) {
return true;
} else {
- // reset
- currentTabletInsertionEventsIterator = null;
- currentTsFileInsertionEvent = null;
+ finishCurrentTsFileInsertionEvent();
return hasNext();
}
}
@@ -467,12 +465,11 @@ public class SubscriptionPipeTabletEventBatch extends
SubscriptionPipeEventBatch
}
}
if (!currentTabletInsertionEventsIterator.hasNext()) {
- iterationSnapshot.addIteratedEnrichedEvent((EnrichedEvent)
currentTsFileInsertionEvent);
+ finishCurrentTsFileInsertionEvent();
}
return convertToTablets(tabletInsertionEvent);
} else {
- currentTabletInsertionEventsIterator = null;
- currentTsFileInsertionEvent = null;
+ finishCurrentTsFileInsertionEvent();
}
}
@@ -517,4 +514,16 @@ public class SubscriptionPipeTabletEventBatch extends
SubscriptionPipeEventBatch
return null;
}
}
+
+ private void finishCurrentTsFileInsertionEvent() {
+ final PipeTsFileInsertionEvent event = (PipeTsFileInsertionEvent)
currentTsFileInsertionEvent;
+ iterationSnapshot.addIteratedEnrichedEvent(event);
+
+ // Release the parser reservation before opening the next file in the same
region. Waiting for
+ // the whole batch to be ACKed can block that file behind this exhausted
parser. The parsed
+ // tablet events remain referenced by the iteration snapshot until ACK or
cleanup.
+ event.close();
+ currentTabletInsertionEventsIterator = null;
+ currentTsFileInsertionEvent = null;
+ }
}
diff --git
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/event/batch/SubscriptionPipeTabletEventBatchTest.java
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/event/batch/SubscriptionPipeTabletEventBatchTest.java
new file mode 100644
index 00000000000..d0d67f4e9ac
--- /dev/null
+++
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/subscription/event/batch/SubscriptionPipeTabletEventBatchTest.java
@@ -0,0 +1,256 @@
+/*
+ * 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.subscription.event.batch;
+
+import org.apache.iotdb.commons.conf.CommonConfig;
+import org.apache.iotdb.commons.conf.CommonDescriptor;
+import org.apache.iotdb.commons.pipe.datastructure.pattern.TablePattern;
+import org.apache.iotdb.db.pipe.event.common.tsfile.PipeTsFileInsertionEvent;
+import org.apache.iotdb.db.pipe.resource.PipeDataNodeResourceManager;
+import org.apache.iotdb.db.pipe.resource.memory.PipeMemoryManager;
+import
org.apache.iotdb.db.pipe.resource.memory.PipeMemoryManager.TsFileParserMemoryReservation;
+import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+import
org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResourceStatus;
+import
org.apache.iotdb.db.subscription.broker.SubscriptionPrefetchingTabletQueue;
+import
org.apache.iotdb.db.subscription.event.pipe.SubscriptionPipeTabletBatchEvents;
+
+import org.apache.tsfile.enums.ColumnCategory;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.external.commons.io.FileUtils;
+import org.apache.tsfile.file.metadata.IDeviceID;
+import org.apache.tsfile.file.metadata.TableSchema;
+import org.apache.tsfile.utils.Pair;
+import org.apache.tsfile.write.TsFileWriter;
+import org.apache.tsfile.write.record.Tablet;
+import org.apache.tsfile.write.schema.MeasurementSchema;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class SubscriptionPipeTabletEventBatchTest {
+
+ private static final String REGION_ID = "7";
+
+ private final CommonConfig config =
CommonDescriptor.getInstance().getConfig();
+ private final PipeMemoryManager memoryManager =
PipeDataNodeResourceManager.memory();
+ private final List<PipeTsFileInsertionEvent> tsFileEvents = new
ArrayList<>();
+
+ private long originalParserMemory;
+ private int originalGlobalLimit;
+ private int originalRegionLimit;
+ private File temporaryDirectory;
+ private SubscriptionPipeTabletEventBatch batch;
+
+ @Before
+ public void setUp() throws Exception {
+ originalParserMemory = config.getPipeTsFileParserMemory();
+ originalGlobalLimit = config.getPipeTsFileParserInFlightMaxNum();
+ originalRegionLimit =
config.getPipeTsFileParserInFlightMaxNumPerPipeRegion();
+ config.setPipeTsFileParserMemory(1);
+ config.setPipeTsFileParserInFlightMaxNum(4);
+ config.setPipeTsFileParserInFlightMaxNumPerPipeRegion(1);
+
+ temporaryDirectory =
Files.createTempDirectory("subscription-tablet-batch").toFile();
+ final SubscriptionPrefetchingTabletQueue queue =
+ Mockito.mock(SubscriptionPrefetchingTabletQueue.class);
+ Mockito.when(queue.getTopicName()).thenReturn("topic");
+ Mockito.when(queue.getConsumerGroupId()).thenReturn("group");
+ batch =
+ new SubscriptionPipeTabletEventBatch(Integer.parseInt(REGION_ID),
queue, 20, 1024 * 1024);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (batch != null) {
+ batch.cleanUp(true);
+ }
+ for (final PipeTsFileInsertionEvent event : tsFileEvents) {
+ event.close();
+ }
+ config.setPipeTsFileParserMemory(originalParserMemory);
+ config.setPipeTsFileParserInFlightMaxNum(originalGlobalLimit);
+ config.setPipeTsFileParserInFlightMaxNumPerPipeRegion(originalRegionLimit);
+ if (temporaryDirectory != null) {
+ FileUtils.deleteDirectory(temporaryDirectory);
+ }
+ }
+
+ @Test(timeout = 10000)
+ public void testSameRegionFilesReleaseParserBeforeBatchAck() throws
Exception {
+ final long baselineTabletMemory =
memoryManager.getUsedMemorySizeInBytesOfTablets();
+ batch.enrichedEvents.add(createTsFileEvent("first.tsfile", 1, false,
false));
+ batch.enrichedEvents.add(createTsFileEvent("second.tsfile", 2, false,
false));
+ batch.resetForIteration();
+
+ Assert.assertTrue(batch.hasNext());
+ final Pair<String, List<Tablet>> first = batch.next();
+ assertTablet(first, "table_0", 1);
+ Assert.assertTrue("The exhausted file must release its parser before ACK",
canReserveParser());
+
+ Assert.assertTrue(batch.hasNext());
+ assertTablet(batch.next(), "table_0", 2);
+ Assert.assertFalse(batch.hasNext());
+ Assert.assertTrue(canReserveParser());
+
+ // Closing a parser must leave its materialized tablets available until
the response is ACKed.
+ assertTablet(first, "table_0", 1);
+ final SubscriptionPipeTabletIterationSnapshot snapshot =
batch.sendIterationSnapshot();
+ Assert.assertEquals(2, snapshot.getIteratedEnrichedEvents().size());
+ snapshot.ack();
+ Assert.assertEquals(baselineTabletMemory,
memoryManager.getUsedMemorySizeInBytesOfTablets());
+ }
+
+ @Test(timeout = 10000)
+ public void testFilteredFileReleasesParserWithoutProducingTablet() throws
Exception {
+ final long baselineTabletMemory =
memoryManager.getUsedMemorySizeInBytesOfTablets();
+ batch.enrichedEvents.add(createTsFileEvent("filtered.tsfile", 1, true,
false));
+ batch.resetForIteration();
+
+ Assert.assertTrue(batch.hasNext());
+ Assert.assertNull(batch.next());
+ Assert.assertTrue("An empty iteration must also release its parser",
canReserveParser());
+ Assert.assertFalse(batch.hasNext());
+
+ final SubscriptionPipeTabletIterationSnapshot snapshot =
batch.sendIterationSnapshot();
+ Assert.assertEquals(1, snapshot.getIteratedEnrichedEvents().size());
+ snapshot.ack();
+ Assert.assertEquals(baselineTabletMemory,
memoryManager.getUsedMemorySizeInBytesOfTablets());
+ }
+
+ @Test(timeout = 10000)
+ public void testDetachedSnapshotCleanupClosesPartiallyParsedFile() throws
Exception {
+ final long baselineTabletMemory =
memoryManager.getUsedMemorySizeInBytesOfTablets();
+ batch.enrichedEvents.add(createTsFileEvent("partial.tsfile", 1, false,
true));
+ batch.resetForIteration();
+
+ Assert.assertNotNull(batch.next());
+ Assert.assertTrue(batch.hasNext());
+ Assert.assertFalse("A file still being parsed must retain its parser",
canReserveParser());
+
+ final SubscriptionPipeTabletBatchEvents events = new
SubscriptionPipeTabletBatchEvents(batch);
+ events.receiveIterationSnapshot(batch.sendIterationSnapshot());
+ events.cleanUp(true);
+ Assert.assertTrue(canReserveParser());
+ Assert.assertFalse(batch.hasNext());
+ Assert.assertEquals(baselineTabletMemory,
memoryManager.getUsedMemorySizeInBytesOfTablets());
+ }
+
+ private boolean canReserveParser() {
+ final TsFileParserMemoryReservation reservation = new
TsFileParserMemoryReservation();
+ final boolean reserved =
+ memoryManager.tryReserveTsFileParserMemory(null, 0, REGION_ID,
reservation);
+ try {
+ return reserved;
+ } finally {
+ if (reserved) {
+ memoryManager.releaseTsFileParserMemory(null, 0, REGION_ID);
+ } else {
+ memoryManager.cancelTsFileParserMemoryReservation(null, 0, REGION_ID,
reservation);
+ }
+ }
+ }
+
+ private PipeTsFileInsertionEvent createTsFileEvent(
+ final String fileName, final long timestamp, final boolean filtered,
final boolean twoTables)
+ throws Exception {
+ final File partitionDirectory = new File(temporaryDirectory, REGION_ID +
File.separator + "0");
+ Assert.assertTrue(partitionDirectory.isDirectory() ||
partitionDirectory.mkdirs());
+ final File tsFile = new File(partitionDirectory, fileName);
+ try (final TsFileWriter writer = new TsFileWriter(tsFile)) {
+ writeTable(writer, "table_0", timestamp);
+ if (twoTables) {
+ writeTable(writer, "table_1", timestamp + 1);
+ }
+ }
+ final TsFileResource resource = new TsFileResource(tsFile);
+ resource.setStatusForTest(TsFileResourceStatus.NORMAL);
+ final IDeviceID device =
+ IDeviceID.Factory.DEFAULT_FACTORY.create(new String[] {"table_0",
"device_0"});
+ resource.updateStartTime(device, timestamp);
+ resource.updateEndTime(device, timestamp + 1);
+ final PipeTsFileInsertionEvent event =
+ new PipeTsFileInsertionEvent(
+ true,
+ "test_sg_0",
+ resource,
+ null,
+ false,
+ false,
+ false,
+ Collections.singleton("table_0"),
+ null,
+ 0,
+ null,
+ null,
+ new TablePattern(true, null, filtered ? "other_table" : null),
+ null,
+ null,
+ null,
+ true,
+ Long.MIN_VALUE,
+ Long.MAX_VALUE);
+ tsFileEvents.add(event);
+ return event;
+ }
+
+ private static void writeTable(final TsFileWriter writer, final String
tableName, final long time)
+ throws Exception {
+ final List<ColumnCategory> categories = Arrays.asList(ColumnCategory.TAG,
ColumnCategory.FIELD);
+ writer.registerTableSchema(
+ new TableSchema(
+ tableName,
+ Arrays.asList(
+ new MeasurementSchema("device_id", TSDataType.STRING),
+ new MeasurementSchema("sensor", TSDataType.INT64)),
+ categories));
+ final Tablet tablet =
+ new Tablet(
+ tableName,
+ Arrays.asList("device_id", "sensor"),
+ Arrays.asList(TSDataType.STRING, TSDataType.INT64),
+ categories,
+ 1);
+ tablet.addTimestamp(0, time);
+ tablet.addValue(0, 0, "device_0");
+ tablet.addValue(0, 1, time);
+ writer.writeTable(tablet);
+ }
+
+ private static void assertTablet(
+ final Pair<String, List<Tablet>> tablets, final String tableName, final
long time) {
+ Assert.assertNotNull(tablets);
+ Assert.assertEquals("test_sg_0", tablets.left);
+ Assert.assertEquals(1, tablets.right.size());
+ final Tablet tablet = tablets.right.get(0);
+ Assert.assertEquals(tableName, tablet.getTableName());
+ Assert.assertEquals(1, tablet.getRowSize());
+ Assert.assertEquals(time, tablet.getTimestamps()[0]);
+ Assert.assertEquals(time, ((long[]) tablet.getValues()[1])[0]);
+ }
+}