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

JackieTien97 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 ab881319f22 Fix fragment failure race with concurrent cancellation 
(#18439)
ab881319f22 is described below

commit ab881319f22df935abb6a4185037c41c8b3e3575
Author: Jackie Tien <[email protected]>
AuthorDate: Tue Aug 11 14:09:18 2026 +0800

    Fix fragment failure race with concurrent cancellation (#18439)
---
 .../fragment/FragmentInstanceManager.java          |  19 +--
 .../fragment/FragmentInstanceManagerTest.java      | 131 +++++++++++++++++++++
 2 files changed, 143 insertions(+), 7 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceManager.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceManager.java
index 5ef70a99354..342dec0a7ba 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceManager.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceManager.java
@@ -64,6 +64,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
 
 import static java.util.Objects.requireNonNull;
 import static 
org.apache.iotdb.calc.execution.schedule.queue.IndexedBlockingQueue.TOO_MANY_CONCURRENT_QUERIES_ERROR_MSG;
@@ -141,6 +142,7 @@ public class FragmentInstanceManager {
     FragmentInstanceId instanceId = instance.getId();
     AtomicLong driversCount = new AtomicLong();
     try (SetThreadName fragmentInstanceName = new 
SetThreadName(instanceId.getFullId())) {
+      AtomicReference<FragmentInstanceInfo> failedInstanceInfo = new 
AtomicReference<>();
       FragmentInstanceExecution execution =
           instanceExecution.computeIfAbsent(
               instanceId,
@@ -228,6 +230,10 @@ public class FragmentInstanceManager {
                         
DataNodeQueryMessages.ERROR_WHEN_CREATE_FRAGMENTINSTANCEEXECUTION, t);
                     stateMachine.failed(t);
                   }
+                  // cancelTask may remove the context from instanceContext 
while this execution is
+                  // still being created. Capture the failure result from the 
local context before
+                  // returning from computeIfAbsent instead of looking it up 
from the map later.
+                  failedInstanceInfo.set(context.getInstanceInfo());
                   clearFIRelatedResources(instanceId);
                   return null;
                 }
@@ -244,7 +250,7 @@ public class FragmentInstanceManager {
                 });
         return execution.getInstanceInfo();
       } else {
-        return createFailedInstanceInfo(instanceId);
+        return failedInstanceInfo.get();
       }
     } finally {
       QueryRelatedResourceMetricSet.getInstance()
@@ -297,6 +303,7 @@ public class FragmentInstanceManager {
   public FragmentInstanceInfo execSchemaQueryFragmentInstance(
       FragmentInstance instance, ISchemaRegion schemaRegion) {
     FragmentInstanceId instanceId = instance.getId();
+    AtomicReference<FragmentInstanceInfo> failedInstanceInfo = new 
AtomicReference<>();
     FragmentInstanceExecution execution =
         instanceExecution.computeIfAbsent(
             instanceId,
@@ -359,6 +366,9 @@ public class FragmentInstanceManager {
                   logger.warn(DataNodeQueryMessages.EXECUTE_ERROR_CAUSED_BY, 
t);
                   stateMachine.failed(t);
                 }
+                // See execDataQueryFragmentInstance for why this result must 
not be fetched from
+                // instanceContext after computeIfAbsent returns.
+                failedInstanceInfo.set(context.getInstanceInfo());
                 clearFIRelatedResources(instanceId);
                 return null;
               }
@@ -374,7 +384,7 @@ public class FragmentInstanceManager {
               });
       return execution.getInstanceInfo();
     } else {
-      return createFailedInstanceInfo(instanceId);
+      return failedInstanceInfo.get();
     }
   }
 
@@ -447,11 +457,6 @@ public class FragmentInstanceManager {
     return statisticsResp == null ? new TFetchFragmentInstanceStatisticsResp() 
: statisticsResp;
   }
 
-  private FragmentInstanceInfo createFailedInstanceInfo(FragmentInstanceId 
instanceId) {
-    FragmentInstanceContext context = instanceContext.get(instanceId);
-    return context.getInstanceInfo();
-  }
-
   private void removeOldInstances() {
     long oldestAllowedInstance = System.currentTimeMillis() - 
infoCacheTime.toMillis();
     instanceContext
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceManagerTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceManagerTest.java
new file mode 100644
index 00000000000..d04fb263a31
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/fragment/FragmentInstanceManagerTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.queryengine.execution.fragment;
+
+import org.apache.iotdb.commons.exception.MetadataLeaseFencedException;
+import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.queryengine.common.FragmentInstanceId;
+import org.apache.iotdb.db.queryengine.common.PlanFragmentId;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.FragmentInstance;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.PlanFragment;
+import org.apache.iotdb.db.schemaengine.schemaregion.ISchemaRegion;
+import org.apache.iotdb.db.storageengine.dataregion.IDataRegionForQuery;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import static 
org.apache.iotdb.commons.exception.MetadataLeaseFencedException.LeaseFencedRetryPolicy.RETRY_UNTIL_SUCCESS;
+import static org.awaitility.Awaitility.await;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class FragmentInstanceManagerTest {
+
+  private static final String FAILURE_MESSAGE = "metadata lease fenced";
+
+  @BeforeClass
+  public static void setUpClass() {
+    IoTDBDescriptor.getInstance().getConfig().setDataNodeId(1);
+  }
+
+  @Test
+  public void testDataQueryFailureInfoAfterConcurrentContextRemoval() throws 
Exception {
+    assertFailureInfoAfterConcurrentContextRemoval(true);
+  }
+
+  @Test
+  public void testSchemaQueryFailureInfoAfterConcurrentContextRemoval() throws 
Exception {
+    assertFailureInfoAfterConcurrentContextRemoval(false);
+  }
+
+  private void assertFailureInfoAfterConcurrentContextRemoval(boolean 
dataQuery) throws Exception {
+    String queryType = dataQuery ? "data" : "schema";
+    FragmentInstanceId instanceId =
+        new FragmentInstanceId(
+            new PlanFragmentId(new QueryId(queryType + "_context_removal"), 
0), "0");
+    FragmentInstance instance = mock(FragmentInstance.class);
+    PlanFragment fragment = mock(PlanFragment.class);
+    CountDownLatch planningStarted = new CountDownLatch(1);
+    CountDownLatch failPlanning = new CountDownLatch(1);
+
+    when(instance.getId()).thenReturn(instanceId);
+    when(instance.getFragment()).thenReturn(fragment);
+    when(instance.getDataNodeFINum()).thenReturn(1);
+    when(instance.getTimeOut()).thenReturn(TimeUnit.SECONDS.toMillis(30));
+    when(fragment.getPlanNodeTree())
+        .thenAnswer(
+            ignored -> {
+              planningStarted.countDown();
+              if (!failPlanning.await(10, TimeUnit.SECONDS)) {
+                throw new AssertionError("Timed out waiting to fail planning");
+              }
+              throw new MetadataLeaseFencedException(FAILURE_MESSAGE, 
RETRY_UNTIL_SUCCESS);
+            });
+
+    FragmentInstanceManager manager = FragmentInstanceManager.getInstance();
+    ExecutorService executor = Executors.newFixedThreadPool(2);
+    try {
+      Future<FragmentInstanceInfo> executionFuture =
+          executor.submit(
+              () ->
+                  dataQuery
+                      ? manager.execDataQueryFragmentInstance(
+                          instance, mock(IDataRegionForQuery.class))
+                      : manager.execSchemaQueryFragmentInstance(
+                          instance, mock(ISchemaRegion.class)));
+
+      assertTrue(planningStarted.await(10, TimeUnit.SECONDS));
+      Future<FragmentInstanceInfo> cancellationFuture =
+          executor.submit(() -> manager.cancelTask(instanceId, true));
+
+      // cancelTask removes the context before it waits for the in-progress 
computeIfAbsent on the
+      // execution map. This recreates the race that previously made the 
failure path look up a
+      // null context.
+      await().atMost(10, TimeUnit.SECONDS).until(() -> 
manager.getInstanceInfo(instanceId) == null);
+      failPlanning.countDown();
+
+      FragmentInstanceInfo failureInfo = executionFuture.get(10, 
TimeUnit.SECONDS);
+      assertNotNull(failureInfo);
+      assertTrue(failureInfo.getState().isFailed());
+      assertEquals(FAILURE_MESSAGE, failureInfo.getMessage());
+      assertTrue(failureInfo.getErrorCode().isPresent());
+      assertEquals(
+          TSStatusCode.METADATA_LEASE_FENCED_RETRY_REQUIRED.getStatusCode(),
+          failureInfo.getErrorCode().get().getCode());
+      assertNotNull(cancellationFuture.get(10, TimeUnit.SECONDS));
+    } finally {
+      failPlanning.countDown();
+      executor.shutdownNow();
+      assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS));
+      manager.cancelTask(instanceId, true);
+    }
+  }
+}

Reply via email to