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

JackieTien97 pushed a commit to branch codex/fix-shutdown-interruption-logs
in repository https://gitbox.apache.org/repos/asf/iotdb.git


The following commit(s) were added to 
refs/heads/codex/fix-shutdown-interruption-logs by this push:
     new a37eade0251 Propagate remote write interruption provenance
a37eade0251 is described below

commit a37eade0251adcfaa3263eba15597d69a4ac1f3b
Author: JackieTien97 <[email protected]>
AuthorDate: Tue Oct 20 20:40:23 2026 +0800

    Propagate remote write interruption provenance
---
 .../iotdb/db/i18n/DataNodeQueryMessages.java       |   1 +
 .../iotdb/db/i18n/DataNodeQueryMessages.java       |   1 +
 .../queryengine/execution/QueryStateMachine.java   |  49 ++++++-
 .../plan/execution/ExecutionResult.java            |   2 +-
 .../queryengine/plan/execution/QueryExecution.java |  18 ++-
 .../plan/scheduler/ClusterScheduler.java           |   3 +-
 .../plan/scheduler/FragInstanceDispatchResult.java |  12 ++
 .../scheduler/FragmentInstanceDispatcherImpl.java  |   6 +-
 .../execution/QueryStateMachineTest.java           |  35 +++++
 .../metrics/IoTDBInternalLocalReporterTest.java    | 157 ++++++++++++++++++++-
 10 files changed, 265 insertions(+), 19 deletions(-)

diff --git 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index 123018f0eb0..35a2cf57f40 100644
--- 
a/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/en/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -1243,6 +1243,7 @@ public final class DataNodeQueryMessages {
       "Interrupted when dispatching read async";
   public static final String INTERRUPTED_WHEN_DISPATCHING_WRITE_ASYNC =
       "Interrupted when dispatching write async";
+  public static final String MESSAGE_INTERRUPTED_ERRORS_ARG_876F1B4B = 
"Interrupted errors: %s";
   public static final String DESERIALIZE_CONSENSUSGROUPID_FAILED =
       "Deserialize ConsensusGroupId failed. ";
   public static final String CAN_T_CONNECT_TO_NODE =
diff --git 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
index 7c77e7218f3..a32008d51ad 100644
--- 
a/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
+++ 
b/iotdb-core/datanode/src/main/i18n/zh/org/apache/iotdb/db/i18n/DataNodeQueryMessages.java
@@ -1223,6 +1223,7 @@ public final class DataNodeQueryMessages {
       "异步分发读取操作时被中断";
   public static final String INTERRUPTED_WHEN_DISPATCHING_WRITE_ASYNC =
       "异步分发写入操作时被中断";
+  public static final String MESSAGE_INTERRUPTED_ERRORS_ARG_876F1B4B = 
"中断错误:%s";
   public static final String DESERIALIZE_CONSENSUSGROUPID_FAILED =
       "反序列化 ConsensusGroupId 失败。";
   public static final String CAN_T_CONNECT_TO_NODE =
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachine.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachine.java
index 1bc3e2b0f32..2683fec32cf 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachine.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachine.java
@@ -54,7 +54,7 @@ public class QueryStateMachine {
   private final StateMachine<QueryState> queryState;
 
   private final AtomicReference<Throwable> failureException = new 
AtomicReference<>();
-  private final AtomicReference<TSStatus> failureStatus = new 
AtomicReference<>();
+  private final AtomicReference<FailureStatus> failureStatus = new 
AtomicReference<>();
 
   public QueryStateMachine(QueryId queryId, ExecutorService executor) {
     this.queryState =
@@ -90,7 +90,7 @@ public class QueryStateMachine {
   }
 
   public void transitionToPendingRetry(TSStatus failureStatus) {
-    this.failureStatus.compareAndSet(null, failureStatus);
+    recordFailureStatus(failureStatus, false);
     queryState.setIf(PENDING_RETRY, currentState -> currentState == 
DISPATCHING);
   }
 
@@ -110,7 +110,7 @@ public class QueryStateMachine {
   }
 
   public boolean transitionToCanceled(Throwable throwable, TSStatus 
failureStatus) {
-    this.failureStatus.compareAndSet(null, failureStatus);
+    recordFailureStatus(failureStatus, false);
     this.failureException.compareAndSet(null, throwable);
     return transitionToDoneState(CANCELED);
   }
@@ -129,10 +129,34 @@ public class QueryStateMachine {
   }
 
   public void transitionToFailed(TSStatus failureStatus) {
-    this.failureStatus.compareAndSet(null, failureStatus);
+    transitionToFailed(failureStatus, false);
+  }
+
+  public void transitionToFailed(TSStatus failureStatus, boolean 
failureCausedByInterruption) {
+    recordFailureStatus(failureStatus, failureCausedByInterruption);
     transitionToDoneState(FAILED);
   }
 
+  private void recordFailureStatus(TSStatus status, boolean 
failureCausedByInterruption) {
+    if (status != null) {
+      // Publish the status and its origin together. A later interruption must 
not reclassify
+      // the first recorded failure.
+      failureStatus.compareAndSet(null, new FailureStatus(status, 
failureCausedByInterruption));
+    }
+  }
+
+  /** Only the exact recorded status carries the interruption provenance 
stored with it. */
+  public boolean isFailureCausedByInterruption(TSStatus status) {
+    FailureStatus failure = failureStatus.get();
+    if (failure == null || failure.status != status || 
!failure.failureCausedByInterruption) {
+      return false;
+    }
+    // Statuses and exceptions can be recorded independently. Keep an 
unrelated exception
+    // visible even if a subsequent dispatch interruption supplied the status.
+    Throwable throwable = failureException.get();
+    return throwable == null || getRootCause(throwable) instanceof 
InterruptedException;
+  }
+
   private boolean transitionToDoneState(QueryState doneState) {
     requireNonNull(doneState, 
DataNodeQueryMessages.EXCEPTION_DONESTATE_IS_NULL_D88F77E5);
     checkArgument(
@@ -161,9 +185,9 @@ public class QueryStateMachine {
   }
 
   public TSStatus getFailureStatus() {
-    TSStatus status = failureStatus.get();
+    FailureStatus status = failureStatus.get();
     if (status != null) {
-      return status;
+      return status.status;
     } else {
       Throwable throwable = failureException.get();
       if (throwable != null) {
@@ -174,7 +198,18 @@ public class QueryStateMachine {
           return RpcUtils.getStatus(((IoTDBException) t).getErrorCode(), 
t.getMessage());
         }
       }
-      return failureStatus.get();
+      status = failureStatus.get();
+      return status == null ? null : status.status;
+    }
+  }
+
+  private static class FailureStatus {
+    private final TSStatus status;
+    private final boolean failureCausedByInterruption;
+
+    private FailureStatus(TSStatus status, boolean 
failureCausedByInterruption) {
+      this.status = status;
+      this.failureCausedByInterruption = failureCausedByInterruption;
     }
   }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/ExecutionResult.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/ExecutionResult.java
index c06ee8b3954..2f46460635a 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/ExecutionResult.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/ExecutionResult.java
@@ -26,7 +26,7 @@ public class ExecutionResult {
   public final QueryId queryId;
   public final TSStatus status;
 
-  /** Whether this status was generated by an interrupted wait without an 
existing query failure. */
+  /** Whether this failure originated from an interrupted wait rather than 
another query error. */
   public final boolean failureCausedByInterruption;
 
   public ExecutionResult(QueryId queryId, TSStatus status) {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
index 49a7a375a33..b8f753d70a0 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/execution/QueryExecution.java
@@ -644,11 +644,14 @@ public class QueryExecution implements IQueryExecution {
     } catch (InterruptedException e) {
       // TODO: (xingtanzjr) use more accurate error handling
       Thread.currentThread().interrupt();
-      // An interrupted waiter can still observe a failure recorded before the 
interruption.
-      // Preserve its identity so callers do not mistake that failure for 
shutdown cancellation.
+      // Dispatch may already have converted an interruption to a failure 
status. Preserve its
+      // recorded origin without reclassifying an unrelated failure.
       TSStatus failureStatus = stateMachine.getFailureStatus();
       if (failureStatus != null) {
-        return new ExecutionResult(context.getQueryId(), failureStatus);
+        return new ExecutionResult(
+            context.getQueryId(),
+            failureStatus,
+            stateMachine.isFailureCausedByInterruption(failureStatus));
       }
       return new ExecutionResult(
           context.getQueryId(),
@@ -656,12 +659,14 @@ public class QueryExecution implements IQueryExecution {
           // A terminal query may have failed with an exception that has no 
dedicated status code.
           !stateMachine.getState().isDone());
     } catch (ExecutionException e) {
+      TSStatus failureStatus = stateMachine.getFailureStatus();
       return new ExecutionResult(
           context.getQueryId(),
-          stateMachine.getFailureStatus() == null
+          failureStatus == null
               ? RpcUtils.getStatus(
                   TSStatusCode.INTERNAL_SERVER_ERROR, 
stateMachine.getFailureMessage())
-              : stateMachine.getFailureStatus());
+              : failureStatus,
+          stateMachine.isFailureCausedByInterruption(failureStatus));
     }
   }
 
@@ -732,7 +737,8 @@ public class QueryExecution implements IQueryExecution {
         || 
IoTDBConstant.ClientVersion.V_1_0.equals(context.getSession().getVersion())) {
       planner.setRedirectInfo(analysis, CONFIG.getAddressAndPort(), tsstatus);
     }
-    return new ExecutionResult(context.getQueryId(), tsstatus);
+    return new ExecutionResult(
+        context.getQueryId(), tsstatus, 
stateMachine.isFailureCausedByInterruption(tsstatus));
   }
 
   public DistributedQueryPlan getDistributedPlan() {
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/ClusterScheduler.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/ClusterScheduler.java
index 5e0aa02f5c0..4c828c3808c 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/ClusterScheduler.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/ClusterScheduler.java
@@ -125,7 +125,8 @@ public class ClusterScheduler implements IScheduler {
         if (needRetry(result.getFailureStatus())) {
           stateMachine.transitionToPendingRetry(result.getFailureStatus());
         } else {
-          stateMachine.transitionToFailed(result.getFailureStatus());
+          stateMachine.transitionToFailed(
+              result.getFailureStatus(), 
result.isFailureCausedByInterruption());
         }
         return;
       }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragInstanceDispatchResult.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragInstanceDispatchResult.java
index 6a621139455..dd9d39694e8 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragInstanceDispatchResult.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragInstanceDispatchResult.java
@@ -26,13 +26,21 @@ public class FragInstanceDispatchResult {
 
   private TSStatus failureStatus;
 
+  private final boolean failureCausedByInterruption;
+
   public FragInstanceDispatchResult(boolean successful) {
     this.successful = successful;
+    this.failureCausedByInterruption = false;
   }
 
   public FragInstanceDispatchResult(TSStatus failureStatus) {
+    this(failureStatus, false);
+  }
+
+  public FragInstanceDispatchResult(TSStatus failureStatus, boolean 
failureCausedByInterruption) {
     this.successful = false;
     this.failureStatus = failureStatus;
+    this.failureCausedByInterruption = failureCausedByInterruption;
   }
 
   public boolean isSuccessful() {
@@ -42,4 +50,8 @@ public class FragInstanceDispatchResult {
   public TSStatus getFailureStatus() {
     return failureStatus;
   }
+
+  public boolean isFailureCausedByInterruption() {
+    return failureCausedByInterruption;
+  }
 }
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragmentInstanceDispatcherImpl.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragmentInstanceDispatcherImpl.java
index 24c42a0c16f..4894a8bc106 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragmentInstanceDispatcherImpl.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/FragmentInstanceDispatcherImpl.java
@@ -330,7 +330,11 @@ public class FragmentInstanceDispatcherImpl implements 
IFragInstanceDispatcher {
       return immediateFuture(
           new FragInstanceDispatchResult(
               RpcUtils.getStatus(
-                  TSStatusCode.INTERNAL_SERVER_ERROR, "Interrupted errors: " + 
e.getMessage())));
+                  TSStatusCode.INTERNAL_SERVER_ERROR,
+                  String.format(
+                      
DataNodeQueryMessages.MESSAGE_INTERRUPTED_ERRORS_ARG_876F1B4B,
+                      e.getMessage())),
+              true));
     }
 
     if (dispatchFailures.isEmpty()) {
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachineTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachineTest.java
index ad7ab094748..118ff7b5025 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachineTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/execution/QueryStateMachineTest.java
@@ -31,13 +31,17 @@ import org.junit.Assert;
 import org.junit.Test;
 
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
 
 public class QueryStateMachineTest {
 
@@ -158,6 +162,37 @@ public class QueryStateMachineTest {
         stateMachine.getFailureStatus().code);
   }
 
+  @Test
+  public void testInterruptionProvenanceBelongsToTheRecordedStatus() {
+    QueryStateMachine stateMachine =
+        new QueryStateMachine(genQueryId(), mock(ExecutorService.class));
+    TSStatus interruption = new 
TSStatus(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode());
+
+    stateMachine.transitionToFailed(interruption, true);
+
+    assertSame(interruption, stateMachine.getFailureStatus());
+    assertTrue(stateMachine.isFailureCausedByInterruption(interruption));
+    assertFalse(
+        stateMachine.isFailureCausedByInterruption(
+            new TSStatus(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode())));
+  }
+
+  @Test
+  public void testLaterInterruptionDoesNotReclassifyExistingFailure() {
+    QueryStateMachine stateMachine =
+        new QueryStateMachine(genQueryId(), mock(ExecutorService.class));
+    TSStatus original = new 
TSStatus(TSStatusCode.WRITE_PROCESS_ERROR.getStatusCode());
+    TSStatus interruption = new 
TSStatus(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode());
+
+    stateMachine.transitionToFailed(original);
+    stateMachine.transitionToFailed(interruption, true);
+    stateMachine.transitionToFailed(original, true);
+
+    assertSame(original, stateMachine.getFailureStatus());
+    assertFalse(stateMachine.isFailureCausedByInterruption(original));
+    assertFalse(stateMachine.isFailureCausedByInterruption(interruption));
+  }
+
   private QueryStateMachine genQueryStateMachine() {
     return new QueryStateMachine(
         genQueryId(), 
IoTDBThreadPoolFactory.newSingleThreadExecutor("TestQueryStateMachine"));
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/metrics/IoTDBInternalLocalReporterTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/metrics/IoTDBInternalLocalReporterTest.java
index 46ab03ac685..a94cefc54c0 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/metrics/IoTDBInternalLocalReporterTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/service/metrics/IoTDBInternalLocalReporterTest.java
@@ -19,9 +19,17 @@
 
 package org.apache.iotdb.db.service.metrics;
 
+import org.apache.iotdb.common.rpc.thrift.TConsensusGroupId;
+import org.apache.iotdb.common.rpc.thrift.TConsensusGroupType;
+import org.apache.iotdb.common.rpc.thrift.TDataNodeLocation;
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TRegionReplicaSet;
 import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.commons.client.IClientManager;
+import 
org.apache.iotdb.commons.client.async.AsyncDataNodeInternalServiceClient;
 import org.apache.iotdb.commons.conf.CommonDescriptor;
 import org.apache.iotdb.commons.queryengine.common.SessionInfo;
+import org.apache.iotdb.commons.queryengine.plan.planner.plan.node.PlanNode;
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
 import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
 import org.apache.iotdb.db.queryengine.common.QueryId;
@@ -29,13 +37,19 @@ import 
org.apache.iotdb.db.queryengine.execution.QueryStateMachine;
 import org.apache.iotdb.db.queryengine.plan.Coordinator;
 import org.apache.iotdb.db.queryengine.plan.analyze.IAnalysis;
 import org.apache.iotdb.db.queryengine.plan.analyze.IPartitionFetcher;
+import org.apache.iotdb.db.queryengine.plan.analyze.QueryType;
 import org.apache.iotdb.db.queryengine.plan.analyze.schema.ISchemaFetcher;
 import org.apache.iotdb.db.queryengine.plan.execution.ExecutionResult;
 import org.apache.iotdb.db.queryengine.plan.execution.QueryExecution;
 import org.apache.iotdb.db.queryengine.plan.planner.IPlanner;
+import org.apache.iotdb.db.queryengine.plan.planner.plan.DistributedQueryPlan;
+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.queryengine.plan.scheduler.ClusterScheduler;
 import org.apache.iotdb.db.queryengine.plan.statement.Statement;
 import org.apache.iotdb.db.queryengine.plan.statement.crud.InsertRowStatement;
 import 
org.apache.iotdb.db.queryengine.plan.statement.metadata.CreateMultiTimeSeriesStatement;
+import org.apache.iotdb.mpp.rpc.thrift.TSendBatchPlanNodeReq;
 import org.apache.iotdb.rpc.TSStatusCode;
 
 import ch.qos.logback.classic.Level;
@@ -52,6 +66,7 @@ import org.mockito.stubbing.OngoingStubbing;
 import org.slf4j.LoggerFactory;
 
 import java.lang.reflect.Field;
+import java.nio.ByteBuffer;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -356,6 +371,138 @@ public class IoTDBInternalLocalReporterTest {
     verifyReloadInterrupt(null, null, false);
   }
 
+  @Test
+  public void testRemoteWriteInterruptedDuringReloadDoesNotWarn() throws 
Exception {
+    verifyRemoteDispatchInterruption(false, false, null);
+  }
+
+  @Test
+  public void testRemoteAutoCreateInterruptedDuringReloadDoesNotWarn() throws 
Exception {
+    verifyRemoteDispatchInterruption(true, false, null);
+  }
+
+  @Test
+  public void testRemoteInterruptionSurvivesClearedThreadFlag() throws 
Exception {
+    verifyRemoteDispatchInterruption(false, true, null);
+  }
+
+  @Test
+  public void testRemoteInterruptionDoesNotReplaceExistingFailure() throws 
Exception {
+    verifyRemoteDispatchInterruption(
+        false, false, new 
TSStatus(TSStatusCode.WRITE_PROCESS_ERROR.getStatusCode()));
+  }
+
+  @Test
+  public void testRemoteInterruptionDoesNotHideExistingException() throws 
Exception {
+    verifyRemoteDispatchInterruption(
+        false, false, null, new IllegalStateException("existing write 
failure"));
+  }
+
+  private void verifyRemoteDispatchInterruption(
+      boolean autoCreate, boolean clearInterrupt, TSStatus recordedFailure) 
throws Exception {
+    verifyRemoteDispatchInterruption(autoCreate, clearInterrupt, 
recordedFailure, null);
+  }
+
+  private void verifyRemoteDispatchInterruption(
+      boolean autoCreate,
+      boolean clearInterrupt,
+      TSStatus recordedFailure,
+      Throwable recordedException)
+      throws Exception {
+    service = Executors.newSingleThreadScheduledExecutor();
+    reporter = newReporter(service);
+    MPPQueryContext context = mock(MPPQueryContext.class);
+    when(context.getQueryId()).thenReturn(new QueryId("remote_metric"));
+    IPlanner planner = mock(IPlanner.class);
+    when(planner.analyze(context)).thenReturn(mock(IAnalysis.class));
+    ExecutorService notifications = mock(ExecutorService.class);
+    QueryExecution queryExecution = new QueryExecution(planner, context, 
notifications);
+    QueryStateMachine stateMachine = stateMachine(queryExecution);
+    if (recordedFailure != null) {
+      stateMachine.transitionToFailed(recordedFailure);
+    } else if (recordedException != null) {
+      stateMachine.transitionToFailed(recordedException);
+    } else {
+      stateMachine.transitionToPlanned();
+    }
+
+    TEndPoint endpoint = new TEndPoint("192.0.2.1", 10730);
+    TDataNodeLocation remote = new 
TDataNodeLocation().setInternalEndPoint(endpoint);
+    TRegionReplicaSet region =
+        new TRegionReplicaSet(
+            new TConsensusGroupId(TConsensusGroupType.DataRegion, 1),
+            Collections.singletonList(remote));
+    PlanNode node = mock(PlanNode.class);
+    when(node.serializeToByteBuffer()).thenReturn(ByteBuffer.allocate(0));
+    when(node.getChildren()).thenReturn(Collections.emptyList());
+    PlanFragment fragment = mock(PlanFragment.class);
+    when(fragment.getPlanNodeTree()).thenReturn(node);
+    FragmentInstance instance = mock(FragmentInstance.class);
+    when(instance.getHostDataNode()).thenReturn(remote);
+    when(instance.getRegionReplicaSet()).thenReturn(region);
+    when(instance.getFragment()).thenReturn(fragment);
+    DistributedQueryPlan plan = mock(DistributedQueryPlan.class);
+    when(plan.getInstances()).thenReturn(Collections.singletonList(instance));
+
+    @SuppressWarnings("unchecked")
+    IClientManager<TEndPoint, AsyncDataNodeInternalServiceClient> clients =
+        mock(IClientManager.class);
+    AsyncDataNodeInternalServiceClient client = 
mock(AsyncDataNodeInternalServiceClient.class);
+    when(clients.borrowClient(endpoint)).thenReturn(client);
+    CountDownLatch dispatched = new CountDownLatch(1);
+    doAnswer(
+            invocation -> {
+              // Leave the RPC response pending so the real 
AsyncPlanNodeSender waits.
+              dispatched.countDown();
+              return null;
+            })
+        .when(client)
+        .sendBatchPlanNode(any(TSendBatchPlanNodeReq.class), any());
+    ClusterScheduler scheduler =
+        new ClusterScheduler(context, stateMachine, plan, QueryType.WRITE, 
null, null, clients);
+    AtomicReference<ExecutionResult> returnedResult = new AtomicReference<>();
+    OngoingStubbing<ExecutionResult> execution = execution();
+    if (autoCreate) {
+      execution = execution.thenReturn(result(TSStatusCode.METADATA_ERROR));
+    }
+    execution.thenAnswer(
+        invocation -> {
+          scheduler.start();
+          if (clearInterrupt) {
+            Thread.interrupted();
+          }
+          // Deliver the getStatus() listener immediately, also exercising its 
normal return
+          // path when the interrupt flag has been consumed after dispatch.
+          doAnswer(
+                  notification -> {
+                    notification.<Runnable>getArgument(0).run();
+                    return null;
+                  })
+              .when(notifications)
+              .execute(any(Runnable.class));
+          ExecutionResult result = queryExecution.getStatus();
+          returnedResult.set(result);
+          return result;
+        });
+
+    writeMetric();
+    assertTrue(dispatched.await(5, TimeUnit.SECONDS));
+    reporter.stop();
+    assertTrue(service.awaitTermination(5, TimeUnit.SECONDS));
+
+    ExecutionResult result = returnedResult.get();
+    if (recordedFailure != null) {
+      assertSame(recordedFailure, result.status);
+    } else {
+      assertEquals(TSStatusCode.INTERNAL_SERVER_ERROR.getStatusCode(), 
result.status.getCode());
+    }
+    boolean hasRecordedFailure = recordedFailure != null || recordedException 
!= null;
+    assertEquals(hasRecordedFailure, hasWarning());
+    assertEquals(!hasRecordedFailure, result.failureCausedByInterruption);
+    executedStatements(autoCreate ? 2 : 1);
+    verify(client).sendBatchPlanNode(any(TSendBatchPlanNodeReq.class), any());
+  }
+
   private void verifyReloadInterrupt(
       TSStatus recordedFailure, Throwable recordedException, boolean 
autoCreate) throws Exception {
     service = Executors.newSingleThreadScheduledExecutor();
@@ -408,9 +555,7 @@ public class IoTDBInternalLocalReporterTest {
     when(planner.analyze(context)).thenReturn(mock(IAnalysis.class));
     ExecutorService notifications = mock(ExecutorService.class);
     QueryExecution execution = new QueryExecution(planner, context, 
notifications);
-    Field stateMachineField = 
QueryExecution.class.getDeclaredField("stateMachine");
-    stateMachineField.setAccessible(true);
-    QueryStateMachine stateMachine = (QueryStateMachine) 
stateMachineField.get(execution);
+    QueryStateMachine stateMachine = stateMachine(execution);
     if (recordedFailure != null) {
       stateMachine.transitionToFailed(recordedFailure);
     } else if (recordedException != null) {
@@ -428,6 +573,12 @@ public class IoTDBInternalLocalReporterTest {
     return execution;
   }
 
+  private QueryStateMachine stateMachine(QueryExecution execution) throws 
Exception {
+    Field stateMachineField = 
QueryExecution.class.getDeclaredField("stateMachine");
+    stateMachineField.setAccessible(true);
+    return (QueryStateMachine) stateMachineField.get(execution);
+  }
+
   private IoTDBInternalLocalReporter newReporter(ScheduledExecutorService 
executor) {
     return new IoTDBInternalLocalReporter(
         executor, coordinator, sessionInfo, partitionFetcher, schemaFetcher);

Reply via email to