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 d77a81534e3 Fix deletion WAL logging and flush completion ordering 
(#18872)
d77a81534e3 is described below

commit d77a81534e3952c754acc5b833939bc338d831f6
Author: Hongzhi Gao <[email protected]>
AuthorDate: Thu Oct 15 14:34:11 2026 +0800

    Fix deletion WAL logging and flush completion ordering (#18872)
    
    * Fix deletion WAL logging and recovery during memtable flush
    
    * Keep deletion fixes within single-memtable file lifecycle
---
 .../dataregion/memtable/TsFileProcessor.java       |  71 ++++---
 .../dataregion/memtable/TsFileProcessorTest.java   | 217 +++++++++++++++++++++
 2 files changed, 257 insertions(+), 31 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java
index f233355f832..52dab8856a4 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessor.java
@@ -1625,14 +1625,17 @@ public class TsFileProcessor {
     }
   }
 
+  // A processor may exist before its first memtable is created if table 
registration fails.
   public WALFlushListener logDeleteDataNodeInWAL(DeleteDataNode 
deleteDataNode) {
     walEntryNum++;
-    return walNode.log(workMemTable.getMemTableId(), deleteDataNode);
+    return walNode.log(
+        workMemTable == null ? MEMTABLE_NOT_EXIST : 
workMemTable.getMemTableId(), deleteDataNode);
   }
 
   public WALFlushListener logDeleteDataNodeInWAL(RelationalDeleteDataNode 
deleteDataNode) {
     walEntryNum++;
-    return walNode.log(workMemTable.getMemTableId(), deleteDataNode);
+    return walNode.log(
+        workMemTable == null ? MEMTABLE_NOT_EXIST : 
workMemTable.getMemTableId(), deleteDataNode);
   }
 
   public TsFileResource getTsFileResource() {
@@ -2001,39 +2004,45 @@ public class TsFileProcessor {
       }
     }
 
-    try {
+    // Keep deletion registration atomic with writing modifications and 
releasing the memtable.
+    // Acquire the deque monitor first, matching syncReleaseFlushedMemTable's 
lock order.
+    synchronized (flushingMemTables) {
       flushQueryLock.writeLock().lock();
-      Iterator<Pair<ModEntry, IMemTable>> iterator = modsToMemtable.iterator();
-      while (iterator.hasNext()) {
-        Pair<ModEntry, IMemTable> entry = iterator.next();
-        if (entry.right.equals(memTableToFlush)) {
-          this.tsFileResource.getModFileForWrite().write(entry.left);
-          tsFileResource.getModFileForWrite().close();
-          iterator.remove();
-          logger.info(StorageEngineMessages.DELETION_WRITTEN_WHEN_FLUSH, 
entry.left);
+      try {
+        try {
+          Iterator<Pair<ModEntry, IMemTable>> iterator = 
modsToMemtable.iterator();
+          while (iterator.hasNext()) {
+            Pair<ModEntry, IMemTable> entry = iterator.next();
+            if (entry.right.equals(memTableToFlush)) {
+              this.tsFileResource.getModFileForWrite().write(entry.left);
+              tsFileResource.getModFileForWrite().close();
+              iterator.remove();
+              logger.info(StorageEngineMessages.DELETION_WRITTEN_WHEN_FLUSH, 
entry.left);
+            }
+          }
+        } catch (IOException e) {
+          logger.error(
+              StorageEngineMessages
+                  
.STORAGE_LOG_MEET_ERROR_WHEN_WRITING_INTO_MODIFICATIONFILE_FILE_OF_63B5E24A,
+              tsFileResource.getTsFile().getAbsolutePath(),
+              e);
+          
DataNodeExceptionMetrics.getInstance().recordSuspiciousDiskException(e);
         }
-      }
-    } catch (IOException e) {
-      logger.error(
-          StorageEngineMessages
-              
.STORAGE_LOG_MEET_ERROR_WHEN_WRITING_INTO_MODIFICATIONFILE_FILE_OF_63B5E24A,
-          tsFileResource.getTsFile().getAbsolutePath(),
-          e);
-      DataNodeExceptionMetrics.getInstance().recordSuspiciousDiskException(e);
-    } finally {
-      flushQueryLock.writeLock().unlock();
-    }
 
-    if (logger.isDebugEnabled()) {
-      logger.debug(
-          
StorageEngineMessages.STORAGE_LOG_TRY_GET_LOCK_TO_RELEASE_A_MEMTABLE_SIGNAL_B9098E21,
-          dataRegionName,
-          tsFileResource.getTsFile().getAbsolutePath(),
-          memTableToFlush.isSignalMemTable());
-    }
+        if (logger.isDebugEnabled()) {
+          logger.debug(
+              
StorageEngineMessages.STORAGE_LOG_TRY_GET_LOCK_TO_RELEASE_A_MEMTABLE_SIGNAL_B9098E21,
+              dataRegionName,
+              tsFileResource.getTsFile().getAbsolutePath(),
+              memTableToFlush.isSignalMemTable());
+        }
 
-    // For sync flush
-    syncReleaseFlushedMemTable(memTableToFlush);
+        // For sync flush
+        syncReleaseFlushedMemTable(memTableToFlush);
+      } finally {
+        flushQueryLock.writeLock().unlock();
+      }
+    }
     try {
       writer.getTsFileOutput().force();
     } catch (IOException e) {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessorTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessorTest.java
index b4117ec121a..324bf78a37e 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessorTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/storageengine/dataregion/memtable/TsFileProcessorTest.java
@@ -24,6 +24,7 @@ import org.apache.iotdb.commons.exception.MetadataException;
 import org.apache.iotdb.commons.file.SystemFileFactory;
 import org.apache.iotdb.commons.path.AlignedFullPath;
 import org.apache.iotdb.commons.path.AlignedPath;
+import org.apache.iotdb.commons.path.MeasurementPath;
 import org.apache.iotdb.commons.path.NonAlignedFullPath;
 import org.apache.iotdb.commons.path.PartialPath;
 import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNodeId;
@@ -35,14 +36,27 @@ import 
org.apache.iotdb.db.exception.TsFileProcessorException;
 import org.apache.iotdb.db.exception.WriteProcessException;
 import org.apache.iotdb.db.queryengine.common.QueryId;
 import org.apache.iotdb.db.queryengine.execution.fragment.QueryContext;
+import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.DeleteDataNode;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowNode;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertRowsNode;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.InsertTabletNode;
+import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalDeleteDataNode;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertRowNode;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.write.RelationalInsertTabletNode;
 import org.apache.iotdb.db.storageengine.dataregion.DataRegionInfo;
 import org.apache.iotdb.db.storageengine.dataregion.DataRegionTest;
+import 
org.apache.iotdb.db.storageengine.dataregion.modification.DeletionPredicate;
+import 
org.apache.iotdb.db.storageengine.dataregion.modification.ModificationFile;
+import 
org.apache.iotdb.db.storageengine.dataregion.modification.TableDeletionEntry;
+import 
org.apache.iotdb.db.storageengine.dataregion.modification.TreeDeletionEntry;
 import org.apache.iotdb.db.storageengine.dataregion.tsfile.TsFileResource;
+import org.apache.iotdb.db.storageengine.dataregion.wal.WALManager;
+import org.apache.iotdb.db.storageengine.dataregion.wal.buffer.WALEntry;
+import org.apache.iotdb.db.storageengine.dataregion.wal.io.WALReader;
+import org.apache.iotdb.db.storageengine.dataregion.wal.node.WALNode;
+import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALFileUtils;
+import org.apache.iotdb.db.storageengine.dataregion.wal.utils.WALMode;
+import 
org.apache.iotdb.db.storageengine.dataregion.wal.utils.listener.WALFlushListener;
 import org.apache.iotdb.db.storageengine.rescon.memory.PrimitiveArrayManager;
 import org.apache.iotdb.db.storageengine.rescon.memory.SystemInfo;
 import org.apache.iotdb.db.utils.EnvironmentUtils;
@@ -62,6 +76,7 @@ import org.apache.tsfile.read.TsFileReader;
 import org.apache.tsfile.read.TsFileSequenceReader;
 import org.apache.tsfile.read.common.Path;
 import org.apache.tsfile.read.common.RowRecord;
+import org.apache.tsfile.read.common.TimeRange;
 import org.apache.tsfile.read.expression.QueryExpression;
 import org.apache.tsfile.read.query.dataset.QueryDataSet;
 import org.apache.tsfile.read.reader.IPointReader;
@@ -80,6 +95,8 @@ import org.slf4j.LoggerFactory;
 
 import java.io.File;
 import java.io.IOException;
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -87,10 +104,13 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static junit.framework.TestCase.assertTrue;
 import static 
org.apache.iotdb.db.storageengine.dataregion.DataRegionTest.buildInsertRowNodeByTSRecord;
+import static org.awaitility.Awaitility.await;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 
@@ -198,6 +218,203 @@ public class TsFileProcessorTest {
         updatedSchema, 
processor.getWriter().getSchema().getTableSchemaMap().get("table1"));
   }
 
+  @Test
+  public void testDeletionDuringFlushCompletionIsPersisted() throws Exception {
+    WALMode originalWalMode = config.getWalMode();
+    config.setWalMode(WALMode.SYNC);
+    try {
+      processor =
+          new TsFileProcessor(
+              storageGroup,
+              SystemFileFactory.INSTANCE.getFile(filePath),
+              sgInfo,
+              this::closeTsFileProcessor,
+              (tsFileProcessor, updateMap, systemFlushTime) -> {},
+              true);
+      processor.setTsFileProcessorInfo(new TsFileProcessorInfo(sgInfo));
+      sgInfo.initTsFileProcessorInfo(processor);
+      TSRecord record = new TSRecord(deviceId, 1);
+      record.addTuple(DataPoint.getDataPoint(dataType, measurementId, "1"));
+      processor.insert(buildInsertRowNodeByTSRecord(record), new long[5]);
+      processor.setManagedByFlushManager(true);
+      processor.asyncClose();
+      Assert.assertNull(processor.getWorkMemTable());
+
+      MeasurementPath path = new MeasurementPath(deviceId + "." + 
measurementId);
+      TreeDeletionEntry deletion = new TreeDeletionEntry(path, Long.MIN_VALUE, 
Long.MAX_VALUE);
+      FutureTask<Void> flush =
+          new FutureTask<>(
+              () -> {
+                processor.flushOneMemTable();
+                return null;
+              });
+      Thread flushThread = new Thread(flush, "test-flush-deletion-completion");
+      Object flushingMemTables = processor.getFlushingMemTable();
+      long monitorOwner = Thread.currentThread().getId();
+      try {
+        synchronized (flushingMemTables) {
+          flushThread.start();
+          // On the old path the flush has already scanned modifications when 
it reaches this
+          // monitor. The fixed path reaches it before scanning and releasing 
the memtable.
+          await()
+              .atMost(30, TimeUnit.SECONDS)
+              .until(
+                  () -> {
+                    ThreadInfo info =
+                        
ManagementFactory.getThreadMXBean().getThreadInfo(flushThread.getId());
+                    return info != null
+                        && info.getThreadState() == Thread.State.BLOCKED
+                        && info.getLockOwnerId() == monitorOwner
+                        && info.getLockInfo().getIdentityHashCode()
+                            == System.identityHashCode(flushingMemTables);
+                  });
+          assertTrue(processor.deleteDataInMemory(deletion));
+        }
+      } finally {
+        try {
+          flush.get(30, TimeUnit.SECONDS);
+        } finally {
+          processor.setManagedByFlushManager(false);
+        }
+      }
+      assertEquals(0, processor.getFlushingMemTableSize());
+      assertEquals(
+          Collections.singletonList(deletion),
+          ModificationFile.readAllModifications(new File(filePath), false));
+    } finally {
+      try {
+        if (processor != null) {
+          processor.syncClose();
+        }
+      } finally {
+        config.setWalMode(originalWalMode);
+      }
+    }
+  }
+
+  @Test
+  public void testTreeDeletionWALWithoutWorkingMemTable() throws Exception {
+    testDeletionWALWithoutWorkingMemTable(false);
+  }
+
+  @Test
+  public void testTableDeletionWALWithoutWorkingMemTable() throws Exception {
+    testDeletionWALWithoutWorkingMemTable(true);
+  }
+
+  private void testDeletionWALWithoutWorkingMemTable(boolean tableModel) 
throws Exception {
+    WALMode originalWalMode = config.getWalMode();
+    config.setWalMode(WALMode.SYNC);
+    try {
+      processor =
+          new TsFileProcessor(
+              storageGroup,
+              SystemFileFactory.INSTANCE.getFile(filePath),
+              sgInfo,
+              this::closeTsFileProcessor,
+              (tsFileProcessor, updateMap, systemFlushTime) -> {},
+              true);
+      processor.setTsFileProcessorInfo(new TsFileProcessorInfo(sgInfo));
+      sgInfo.initTsFileProcessorInfo(processor);
+      WALNode walNode =
+          (WALNode)
+              WALManager.getInstance()
+                  
.applyForWALNode(WALManager.getApplicantUniqueId(storageGroup, true));
+
+      Assert.assertNull(processor.getWorkMemTable());
+      logDeletionForTest(tableModel, 1);
+
+      TSRecord record = new TSRecord(deviceId, 1);
+      record.addTuple(DataPoint.getDataPoint(dataType, measurementId, "1"));
+      processor.insert(buildInsertRowNodeByTSRecord(record), new long[5]);
+      long memTableId = processor.getWorkMemTable().getMemTableId();
+      logDeletionForTest(tableModel, 2);
+
+      // Use the production close path, while keeping flush execution under 
test control.
+      processor.setManagedByFlushManager(true);
+      try {
+        processor.asyncClose();
+        Assert.assertNull(processor.getWorkMemTable());
+        assertEquals(1, processor.getFlushingMemTableSize());
+        processor.flushOneMemTable();
+      } finally {
+        processor.setManagedByFlushManager(false);
+      }
+      assertEquals(0, processor.getFlushingMemTableSize());
+      walNode.rollWALFile();
+
+      List<Long> memTableIds = new ArrayList<>();
+      List<Long> searchIndexes = new ArrayList<>();
+      File[] walFiles = 
WALFileUtils.listAllWALFiles(walNode.getLogDirectory());
+      Assert.assertNotNull(walFiles);
+      WALFileUtils.ascSortByVersionId(walFiles);
+      for (File walFile : walFiles) {
+        try (WALReader reader = new WALReader(walFile)) {
+          while (reader.hasNext()) {
+            WALEntry entry = reader.next();
+            if (entry.getValue() instanceof DeleteDataNode) {
+              Assert.assertFalse(tableModel);
+              DeleteDataNode deletion = (DeleteDataNode) entry.getValue();
+              assertEquals(Long.MIN_VALUE, deletion.getDeleteStartTime());
+              assertEquals(Long.MAX_VALUE, deletion.getDeleteEndTime());
+              assertEquals(
+                  Collections.singletonList(new MeasurementPath(deviceId + "." 
+ measurementId)),
+                  deletion.getPathList());
+              memTableIds.add(entry.getMemTableId());
+              searchIndexes.add(deletion.getSearchIndex());
+            } else if (entry.getValue() instanceof RelationalDeleteDataNode) {
+              Assert.assertTrue(tableModel);
+              RelationalDeleteDataNode deletion = (RelationalDeleteDataNode) 
entry.getValue();
+              assertEquals(
+                  Collections.singletonList(
+                      new TableDeletionEntry(
+                          new DeletionPredicate("t2"),
+                          new TimeRange(Long.MIN_VALUE, Long.MAX_VALUE))),
+                  deletion.getModEntries());
+              memTableIds.add(entry.getMemTableId());
+              searchIndexes.add(deletion.getSearchIndex());
+            }
+          }
+        }
+      }
+      assertEquals(
+          Arrays.asList((long) TsFileProcessor.MEMTABLE_NOT_EXIST, 
memTableId), memTableIds);
+      assertEquals(Arrays.asList(1L, 2L), searchIndexes);
+    } finally {
+      try {
+        if (processor != null) {
+          processor.syncClose();
+        }
+      } finally {
+        config.setWalMode(originalWalMode);
+      }
+    }
+  }
+
+  private void logDeletionForTest(boolean tableModel, long searchIndex) throws 
Exception {
+    WALFlushListener listener;
+    if (tableModel) {
+      RelationalDeleteDataNode deletion =
+          new RelationalDeleteDataNode(
+              new PlanNodeId(""),
+              new TableDeletionEntry(
+                  new DeletionPredicate("t2"), new TimeRange(Long.MIN_VALUE, 
Long.MAX_VALUE)),
+              storageGroup);
+      deletion.setSearchIndex(searchIndex);
+      listener = processor.logDeleteDataNodeInWAL(deletion);
+    } else {
+      DeleteDataNode deletion =
+          new DeleteDataNode(
+              new PlanNodeId(""),
+              Collections.singletonList(new MeasurementPath(deviceId + "." + 
measurementId)),
+              Long.MIN_VALUE,
+              Long.MAX_VALUE);
+      deletion.setSearchIndex(searchIndex);
+      listener = processor.logDeleteDataNodeInWAL(deletion);
+    }
+    assertEquals(WALFlushListener.Status.SUCCESS, listener.waitForResult());
+  }
+
   @Test
   public void testWriteAndFlush()
       throws IOException, WriteProcessException, MetadataException, 
ExecutionException {

Reply via email to