This is an automated email from the ASF dual-hosted git repository.

JackieTien97 pushed a commit to branch dev/1.3
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to refs/heads/dev/1.3 by this push:
     new c056ce6f861 [To dev/1.3] Preserve query memory registrations during 
stale fragment cleanup- #18840
c056ce6f861 is described below

commit c056ce6f8615905e3c54938e969a8074d4f1681f
Author: Jackie Tien <[email protected]>
AuthorDate: Tue Oct 13 11:38:56 2026 +0800

    [To dev/1.3] Preserve query memory registrations during stale fragment 
cleanup- #18840
---
 .../queryengine/execution/memory/MemoryPool.java   |   5 +-
 .../execution/memory/MemoryPoolTest.java           | 128 +++++++++++++++++++++
 2 files changed, 132 insertions(+), 1 deletion(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/memory/MemoryPool.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/memory/MemoryPool.java
index 9a00d5a87de..8682aa904e3 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/memory/MemoryPool.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/memory/MemoryPool.java
@@ -226,7 +226,10 @@ public class MemoryPool {
       }
       synchronized (queryMemoryReservations) {
         queryRelatedMemory.remove(fragmentInstanceId);
-        if (queryRelatedMemory.isEmpty()) {
+        // Another fragment may have recreated the query entry while this 
cleanup waited for the
+        // lock.
+        if (queryRelatedMemory.isEmpty()
+            && queryMemoryReservations.get(queryId) == queryRelatedMemory) {
           queryMemoryReservations.remove(queryId);
         }
       }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/memory/MemoryPoolTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/memory/MemoryPoolTest.java
index cf7e15dc71f..283823c10e3 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/memory/MemoryPoolTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/memory/MemoryPoolTest.java
@@ -19,6 +19,7 @@
 
 package org.apache.iotdb.db.queryengine.execution.memory;
 
+import org.apache.iotdb.db.exception.runtime.MemoryLeakException;
 import 
org.apache.iotdb.db.queryengine.execution.memory.MemoryPool.MemoryReservationResult;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -26,6 +27,14 @@ import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.lang.management.ManagementFactory;
+import java.lang.management.ThreadInfo;
+import java.lang.reflect.Field;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+
+import static org.awaitility.Awaitility.await;
+
 public class MemoryPoolTest {
 
   MemoryPool pool;
@@ -41,6 +50,125 @@ public class MemoryPoolTest {
     pool.registerPlanNodeIdToQueryMemoryMap(QUERY_ID, FRAGMENT_INSTANCE_ID, 
PLAN_NODE_ID);
   }
 
+  @Test
+  public void testDeregisterDoesNotRemoveReregisteredQueryMemory() throws 
Exception {
+    Field reservationsField = 
MemoryPool.class.getDeclaredField("queryMemoryReservations");
+    reservationsField.setAccessible(true);
+    Object reservationsLock = reservationsField.get(pool);
+    long lockOwnerId = Thread.currentThread().getId();
+    FutureTask<Void> deregistration =
+        new FutureTask<>(
+            () -> {
+              // An empty fragment may finish without registering any memory.
+              pool.deRegisterFragmentInstanceFromQueryMemoryMap(QUERY_ID, 
"empty", true);
+              return null;
+            });
+    Thread cleanupThread = new Thread(deregistration, 
"memory-pool-stale-deregister");
+    try {
+      synchronized (reservationsLock) {
+        cleanupThread.start();
+        // Hold the registration lock until the empty fragment cleanup reaches 
it.
+        await()
+            .atMost(10, TimeUnit.SECONDS)
+            .until(
+                () -> {
+                  ThreadInfo info =
+                      
ManagementFactory.getThreadMXBean().getThreadInfo(cleanupThread.getId());
+                  return info != null
+                      && info.getThreadState() == Thread.State.BLOCKED
+                      && info.getLockOwnerId() == lockOwnerId
+                      && info.getLockInfo().getIdentityHashCode()
+                          == System.identityHashCode(reservationsLock);
+                });
+
+        // Replace the query map while the delayed cleanup still refers to the 
old one.
+        pool.deRegisterFragmentInstanceFromQueryMemoryMap(QUERY_ID, 
FRAGMENT_INSTANCE_ID, true);
+        Assert.assertEquals(0, pool.getQueryMemoryReservationSize());
+        pool.registerPlanNodeIdToQueryMemoryMap(QUERY_ID, "result", 
PLAN_NODE_ID);
+        Assert.assertTrue(
+            pool.tryReserveForTest(QUERY_ID, "result", PLAN_NODE_ID, 256L, 
Long.MAX_VALUE));
+      }
+      deregistration.get(10, TimeUnit.SECONDS);
+
+      Assert.assertEquals(1, pool.getQueryMemoryReservationSize());
+      Assert.assertEquals(256L, pool.getQueryMemoryReservedBytes(QUERY_ID));
+      Assert.assertEquals(256L, pool.getReservedBytes());
+      pool.free(QUERY_ID, "result", PLAN_NODE_ID, 256L);
+      pool.deRegisterFragmentInstanceFromQueryMemoryMap(QUERY_ID, "result", 
true);
+      Assert.assertEquals(0, pool.getQueryMemoryReservationSize());
+      Assert.assertEquals(0L, pool.getReservedBytes());
+    } finally {
+      cleanupThread.join(TimeUnit.SECONDS.toMillis(10));
+      Assert.assertFalse(cleanupThread.isAlive());
+    }
+  }
+
+  @Test
+  public void testDeregisterMissingFragmentPreservesReservedMemory() {
+    Assert.assertTrue(
+        pool.tryReserveForTest(QUERY_ID, FRAGMENT_INSTANCE_ID, PLAN_NODE_ID, 
256L, Long.MAX_VALUE));
+
+    pool.deRegisterFragmentInstanceFromQueryMemoryMap(QUERY_ID, "empty", true);
+
+    Assert.assertEquals(1, pool.getQueryMemoryReservationSize());
+    Assert.assertEquals(256L, pool.getQueryMemoryReservedBytes(QUERY_ID));
+    pool.free(QUERY_ID, FRAGMENT_INSTANCE_ID, PLAN_NODE_ID, 256L);
+    pool.deRegisterFragmentInstanceFromQueryMemoryMap(QUERY_ID, 
FRAGMENT_INSTANCE_ID, true);
+    Assert.assertEquals(0, pool.getQueryMemoryReservationSize());
+    Assert.assertEquals(0L, pool.getReservedBytes());
+  }
+
+  @Test
+  public void testDeregisterPreservesOtherFragments() {
+    pool.registerPlanNodeIdToQueryMemoryMap(QUERY_ID, "other", PLAN_NODE_ID);
+    Assert.assertTrue(
+        pool.tryReserveForTest(QUERY_ID, "other", PLAN_NODE_ID, 256L, 
Long.MAX_VALUE));
+
+    pool.deRegisterFragmentInstanceFromQueryMemoryMap(QUERY_ID, 
FRAGMENT_INSTANCE_ID, true);
+
+    Assert.assertEquals(1, pool.getQueryMemoryReservationSize());
+    Assert.assertEquals(256L, pool.getQueryMemoryReservedBytes(QUERY_ID));
+    pool.free(QUERY_ID, "other", PLAN_NODE_ID, 256L);
+    pool.deRegisterFragmentInstanceFromQueryMemoryMap(QUERY_ID, "other", true);
+    Assert.assertEquals(0, pool.getQueryMemoryReservationSize());
+    Assert.assertEquals(0L, pool.getReservedBytes());
+  }
+
+  @Test
+  public void testDeregisterWaitsForMemoryRelease() {
+    Assert.assertTrue(
+        pool.tryReserveForTest(QUERY_ID, FRAGMENT_INSTANCE_ID, PLAN_NODE_ID, 
256L, Long.MAX_VALUE));
+
+    pool.deRegisterFragmentInstanceFromQueryMemoryMap(QUERY_ID, 
FRAGMENT_INSTANCE_ID, false);
+
+    Assert.assertEquals(1, pool.getQueryMemoryReservationSize());
+    Assert.assertEquals(256L, pool.getQueryMemoryReservedBytes(QUERY_ID));
+    pool.free(QUERY_ID, FRAGMENT_INSTANCE_ID, PLAN_NODE_ID, 256L);
+    pool.deRegisterFragmentInstanceFromQueryMemoryMap(QUERY_ID, 
FRAGMENT_INSTANCE_ID, false);
+    Assert.assertEquals(0, pool.getQueryMemoryReservationSize());
+    Assert.assertEquals(0L, pool.getReservedBytes());
+
+    // Repeated cleanup after the query has been removed remains a no-op.
+    pool.deRegisterFragmentInstanceFromQueryMemoryMap(QUERY_ID, 
FRAGMENT_INSTANCE_ID, true);
+    Assert.assertEquals(0, pool.getQueryMemoryReservationSize());
+    Assert.assertEquals(0L, pool.getReservedBytes());
+  }
+
+  @Test
+  public void testForceDeregisterReportsMemoryLeak() {
+    Assert.assertTrue(
+        pool.tryReserveForTest(QUERY_ID, FRAGMENT_INSTANCE_ID, PLAN_NODE_ID, 
256L, Long.MAX_VALUE));
+
+    Assert.assertThrows(
+        MemoryLeakException.class,
+        () ->
+            pool.deRegisterFragmentInstanceFromQueryMemoryMap(
+                QUERY_ID, FRAGMENT_INSTANCE_ID, true));
+
+    Assert.assertEquals(0, pool.getQueryMemoryReservationSize());
+    Assert.assertEquals(256L, pool.getReservedBytes());
+  }
+
   @Test
   public void testTryReserve() {
 

Reply via email to