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

jt2594838 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 1fdcb123ac9 [To dev/1.3] Preserve LOAD failure details in logs and 
responses (#18935) (#18940)
1fdcb123ac9 is described below

commit 1fdcb123ac9f045b014c52eabaa5c9d1707ad936
Author: Caideyipi <[email protected]>
AuthorDate: Tue Oct 27 11:41:11 2026 +0800

    [To dev/1.3] Preserve LOAD failure details in logs and responses (#18935) 
(#18940)
    
    * [To dev/1.3] Preserve LOAD failure details in logs and responses (#18935)
    
    * Fix LoadTsFile analyzer diagnostics test reflection
    
    * Fix unused PowerMock dependency
    
    * Fix PowerMock access to Java reflection internals
    
    * Open Java stream package for PowerMock tests
---
 .../iotdb/db/i18n/DataNodeQueryMessages.java       |  23 ++++
 .../iotdb/db/i18n/DataNodeQueryMessages.java       |  23 ++++
 .../plan/analyze/load/LoadTsFileAnalyzer.java      |  55 +++++---
 .../scheduler/load/LoadTsFileDispatcherImpl.java   |  57 ++++++--
 .../plan/scheduler/load/LoadTsFileScheduler.java   |  41 ++++--
 .../load/LoadTsFileAnalyzerDiagnosticsTest.java    | 147 +++++++++++++++++++++
 .../load/LoadTsFileDispatcherImplTest.java         |  84 +++++++++++-
 pom.xml                                            |   2 +
 8 files changed, 389 insertions(+), 43 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 1c4433d3b55..4d04dbaf1d2 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
@@ -23,6 +23,29 @@ public final class DataNodeQueryMessages {
 
   private DataNodeQueryMessages() {}
 
+  public static final String 
EXCEPTION_FAILED_TO_CONVERT_TSFILE_ARG_TO_TABLETS_STATUS_ARG_E7C9F477 =
+      "Failed to convert TsFile %s to tablets. Status: %s";
+  public static final String 
EXCEPTION_FAILED_TO_CONVERT_TSFILE_ARG_TO_TABLETS_ARG_96358FDD =
+      "Failed to convert TsFile %s to tablets: %s";
+  public static final String 
MESSAGE_TABLET_CONVERSION_RETURNED_NO_STATUS_88BAB5D3 =
+      "Tablet conversion returned no status";
+  public static final String
+      
EXCEPTION_FAILED_TO_LOAD_TSFILES_AFTER_TABLET_CONVERSION_DETAILS_ARG_53647EC5 =
+          "Failed to load TsFiles after tablet conversion. Details: %s";
+  public static final String
+      LOG_LOAD_REJECTED_UUID_ARG_REGION_ARG_ENDPOINT_ARG_STATUS_ARG_8ED0679E =
+          "LOAD rejected: uuid={}, region={}, endpoint={}, status={}";
+  public static final String 
LOG_LOAD_COMMAND_REJECTED_UUID_ARG_ENDPOINT_ARG_STATUS_ARG_E8D0031C =
+      "LOAD command rejected: uuid={}, endpoint={}, status={}";
+  public static final String
+      MESSAGE_TARGET_NODE_REJECTED_LOAD_WITHOUT_A_STATUS_OR_A_MESSAGE_F4E4C645 
=
+          "Target node rejected LOAD without a status or a message";
+  public static final String
+      
MESSAGE_FAILED_TO_DISPATCH_LOAD_ARG_FOR_REGION_ARG_TO_NODE_ARG_ARG_57EFEC3A =
+          "Failed to dispatch LOAD %s for region %s to node %s: %s";
+  public static final String 
MESSAGE_FAILED_TO_DISPATCH_LOAD_COMMAND_ARG_TO_NODE_ARG_ARG_53D2BF0B =
+      "Failed to dispatch LOAD command %s to node %s: %s";
+
   public static final String RESULT_SET_COLUMN_MEMORY_SHORTAGE_EQUIVALENT =
       "The failed memory reservation exceeds available memory by the 
equivalent of at least "
           + "%,d columns, estimated from the observed average column size. ";
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 5b151ce3e35..e84e99433f5 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
@@ -23,6 +23,29 @@ public final class DataNodeQueryMessages {
 
   private DataNodeQueryMessages() {}
 
+  public static final String 
EXCEPTION_FAILED_TO_CONVERT_TSFILE_ARG_TO_TABLETS_STATUS_ARG_E7C9F477 =
+      "将 TsFile %s 转换为 tablet 失败。状态:%s";
+  public static final String 
EXCEPTION_FAILED_TO_CONVERT_TSFILE_ARG_TO_TABLETS_ARG_96358FDD =
+      "将 TsFile %s 转换为 tablet 失败:%s";
+  public static final String 
MESSAGE_TABLET_CONVERSION_RETURNED_NO_STATUS_88BAB5D3 =
+      "tablet 转换未返回状态";
+  public static final String
+      
EXCEPTION_FAILED_TO_LOAD_TSFILES_AFTER_TABLET_CONVERSION_DETAILS_ARG_53647EC5 =
+          "将 TsFile 转换为 tablet 后加载仍失败。详情:%s";
+  public static final String
+      LOG_LOAD_REJECTED_UUID_ARG_REGION_ARG_ENDPOINT_ARG_STATUS_ARG_8ED0679E =
+          "LOAD 被拒绝:uuid={},region={},endpoint={},status={}";
+  public static final String
+      MESSAGE_TARGET_NODE_REJECTED_LOAD_WITHOUT_A_STATUS_OR_A_MESSAGE_F4E4C645 
=
+          "目标节点拒绝 LOAD,且未返回状态或消息";
+  public static final String 
LOG_LOAD_COMMAND_REJECTED_UUID_ARG_ENDPOINT_ARG_STATUS_ARG_E8D0031C =
+      "LOAD command 被拒绝:uuid={},endpoint={},status={}";
+  public static final String
+      
MESSAGE_FAILED_TO_DISPATCH_LOAD_ARG_FOR_REGION_ARG_TO_NODE_ARG_ARG_57EFEC3A =
+          "分发 LOAD %s(region %s)到节点 %s 失败:%s";
+  public static final String 
MESSAGE_FAILED_TO_DISPATCH_LOAD_COMMAND_ARG_TO_NODE_ARG_ARG_53D2BF0B =
+      "分发 LOAD command %s 到节点 %s 失败:%s";
+
   public static final String RESULT_SET_COLUMN_MEMORY_SHORTAGE_EQUIVALENT =
       "本次失败的内存申请超出可用内存,按已记录列的平均内存估算,至少超出相当于 %,d 列的容量。";
 
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzer.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzer.java
index 3e25e033626..4cdf80d5416 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzer.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzer.java
@@ -44,6 +44,7 @@ import 
org.apache.iotdb.db.exception.load.LoadEmptyFileException;
 import org.apache.iotdb.db.exception.load.LoadFileException;
 import org.apache.iotdb.db.exception.load.LoadRuntimeOutOfMemoryException;
 import org.apache.iotdb.db.exception.sql.SemanticException;
+import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
 import org.apache.iotdb.db.protocol.client.ConfigNodeClient;
 import org.apache.iotdb.db.protocol.client.ConfigNodeClientManager;
 import org.apache.iotdb.db.protocol.client.ConfigNodeInfo;
@@ -544,23 +545,43 @@ public class LoadTsFileAnalyzer implements AutoCloseable {
 
     final LoadTsFileDataTypeConverter loadTsFileDataTypeConverter =
         new LoadTsFileDataTypeConverter(context, isGeneratedByPipe);
-    final TSStatus status =
-        loadTsFileStatement.isConvertOnTypeMismatch()
-            ? 
loadTsFileDataTypeConverter.convertForTreeModel(loadTsFileStatement).orElse(null)
-            : null;
-
-    if (status == null) {
-      LOGGER.warn(
-          "Load: Failed to convert to tablets from statement {}. Status is 
null.",
-          loadTsFileStatement);
-      analysis.setFailStatus(
-          new 
TSStatus(TSStatusCode.LOAD_FILE_ERROR.getStatusCode()).setMessage(e.getMessage()));
-    } else if (!loadTsFileDataTypeConverter.isSuccessful(status)) {
-      LOGGER.warn(
-          "Load: Failed to convert to tablets from statement {}. Status: {}",
-          loadTsFileStatement,
-          status);
-      analysis.setFailStatus(status);
+    final String filePaths =
+        loadTsFileStatement.getTsFiles().stream()
+            .map(File::getPath)
+            .collect(Collectors.joining(", "));
+    try {
+      final TSStatus status =
+          loadTsFileStatement.isConvertOnTypeMismatch()
+              ? 
loadTsFileDataTypeConverter.convertForTreeModel(loadTsFileStatement).orElse(null)
+              : null;
+
+      if (status == null) {
+        final String message =
+            String.format(
+                DataNodeQueryMessages
+                    
.EXCEPTION_FAILED_TO_CONVERT_TSFILE_ARG_TO_TABLETS_ARG_96358FDD,
+                filePaths,
+                
DataNodeQueryMessages.MESSAGE_TABLET_CONVERSION_RETURNED_NO_STATUS_88BAB5D3);
+        LOGGER.warn(message);
+        
analysis.setFailStatus(RpcUtils.getStatus(TSStatusCode.LOAD_FILE_ERROR, 
message));
+      } else if (!loadTsFileDataTypeConverter.isSuccessful(status)) {
+        final String message =
+            String.format(
+                DataNodeQueryMessages
+                    
.EXCEPTION_FAILED_TO_CONVERT_TSFILE_ARG_TO_TABLETS_STATUS_ARG_E7C9F477,
+                filePaths,
+                status);
+        LOGGER.warn(message);
+        analysis.setFailStatus(status.deepCopy().setMessage(message));
+      }
+    } catch (final Exception conversionException) {
+      final String message =
+          String.format(
+              
DataNodeQueryMessages.EXCEPTION_FAILED_TO_CONVERT_TSFILE_ARG_TO_TABLETS_ARG_96358FDD,
+              filePaths,
+              conversionException);
+      LOGGER.warn(message, conversionException);
+      analysis.setFailStatus(RpcUtils.getStatus(TSStatusCode.LOAD_FILE_ERROR, 
message));
     }
 
     analysis.setFinishQueryAfterAnalyze(true);
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileDispatcherImpl.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileDispatcherImpl.java
index 542d726186f..7e2a3ff0029 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileDispatcherImpl.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileDispatcherImpl.java
@@ -35,6 +35,7 @@ import 
org.apache.iotdb.commons.consensus.index.ProgressIndexType;
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
 import org.apache.iotdb.db.exception.load.LoadFileException;
 import org.apache.iotdb.db.exception.mpp.FragmentInstanceDispatchException;
+import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
 import org.apache.iotdb.db.pipe.agent.PipeDataNodeAgent;
 import org.apache.iotdb.db.queryengine.plan.planner.plan.FragmentInstance;
 import org.apache.iotdb.db.queryengine.plan.planner.plan.SubPlan;
@@ -317,18 +318,39 @@ public class LoadTsFileDispatcherImpl implements 
IFragInstanceDispatcher, AutoCl
         for (final TTsFilePieceReq loadTsFileReq : loadTsFileReqs) {
           final TLoadResp loadResp = client.sendTsFilePieceNode(loadTsFileReq);
           if (!loadResp.isAccepted()) {
-            LOGGER.warn(loadResp.message);
-            throw new FragmentInstanceDispatchException(loadResp.status);
+            final TSStatus failureStatus =
+                loadResp.isSetStatus()
+                    ? loadResp.getStatus()
+                    : RpcUtils.getStatus(
+                        TSStatusCode.DISPATCH_ERROR,
+                        loadResp.isSetMessage()
+                            ? loadResp.getMessage()
+                            : DataNodeQueryMessages
+                                
.MESSAGE_TARGET_NODE_REJECTED_LOAD_WITHOUT_A_STATUS_OR_A_MESSAGE_F4E4C645);
+            LOGGER.warn(
+                DataNodeQueryMessages
+                    
.LOG_LOAD_REJECTED_UUID_ARG_REGION_ARG_ENDPOINT_ARG_STATUS_ARG_8ED0679E,
+                uuid,
+                consensusGroupId,
+                endPoint,
+                failureStatus);
+            throw new FragmentInstanceDispatchException(failureStatus);
           }
         }
       }
+    } catch (final FragmentInstanceDispatchException e) {
+      throw e;
     } catch (Exception e) {
       adjustTimeoutIfNecessary(e);
 
       final String exceptionMessage =
           String.format(
-              "failed to dispatch load command %s to node %s because of 
exception: %s",
-              uuid, endPoint, e);
+              DataNodeQueryMessages
+                  
.MESSAGE_FAILED_TO_DISPATCH_LOAD_ARG_FOR_REGION_ARG_TO_NODE_ARG_ARG_57EFEC3A,
+              uuid,
+              consensusGroupId,
+              endPoint,
+              e);
       LOGGER.warn(exceptionMessage, e);
       throw new FragmentInstanceDispatchException(
           new TSStatus()
@@ -413,16 +435,35 @@ public class LoadTsFileDispatcherImpl implements 
IFragInstanceDispatcher, AutoCl
 
       final TLoadResp loadResp = client.sendLoadCommand(loadCommandReq);
       if (!loadResp.isAccepted()) {
-        LOGGER.warn(loadResp.message);
-        throw new FragmentInstanceDispatchException(loadResp.status);
+        final TSStatus failureStatus =
+            loadResp.isSetStatus()
+                ? loadResp.getStatus()
+                : RpcUtils.getStatus(
+                    TSStatusCode.DISPATCH_ERROR,
+                    loadResp.isSetMessage()
+                        ? loadResp.getMessage()
+                        : DataNodeQueryMessages
+                            
.MESSAGE_TARGET_NODE_REJECTED_LOAD_WITHOUT_A_STATUS_OR_A_MESSAGE_F4E4C645);
+        LOGGER.warn(
+            DataNodeQueryMessages
+                
.LOG_LOAD_COMMAND_REJECTED_UUID_ARG_ENDPOINT_ARG_STATUS_ARG_E8D0031C,
+            loadCommandReq.uuid,
+            endPoint,
+            failureStatus);
+        throw new FragmentInstanceDispatchException(failureStatus);
       }
+    } catch (final FragmentInstanceDispatchException e) {
+      throw e;
     } catch (Exception e) {
       adjustTimeoutIfNecessary(e);
 
       final String exceptionMessage =
           String.format(
-              "failed to dispatch load command %s to node %s because of 
exception: %s",
-              loadCommandReq, endPoint, e);
+              DataNodeQueryMessages
+                  
.MESSAGE_FAILED_TO_DISPATCH_LOAD_COMMAND_ARG_TO_NODE_ARG_ARG_53D2BF0B,
+              loadCommandReq,
+              endPoint,
+              e);
       LOGGER.warn(exceptionMessage, e);
       throw new FragmentInstanceDispatchException(
           new TSStatus()
diff --git 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileScheduler.java
 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileScheduler.java
index 9c6946be6ab..d1f1f3fb370 100644
--- 
a/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileScheduler.java
+++ 
b/iotdb-core/datanode/src/main/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileScheduler.java
@@ -43,6 +43,7 @@ import org.apache.iotdb.db.exception.load.LoadFileException;
 import org.apache.iotdb.db.exception.load.LoadReadOnlyException;
 import org.apache.iotdb.db.exception.load.RegionReplicaSetChangedException;
 import org.apache.iotdb.db.exception.mpp.FragmentInstanceDispatchException;
+import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
 import org.apache.iotdb.db.pipe.agent.PipeDataNodeAgent;
 import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
 import org.apache.iotdb.db.queryengine.common.PlanFragmentId;
@@ -559,6 +560,7 @@ public class LoadTsFileScheduler implements IScheduler {
   private void convertFailedTsFilesToTabletsAndRetry() {
     final LoadTsFileDataTypeConverter loadTsFileDataTypeConverter =
         new LoadTsFileDataTypeConverter(queryContext, isGeneratedByPipe);
+    final List<String> failureDetails = new ArrayList<>();
 
     final Iterator<Integer> iterator = failedTsFileNodeIndexes.listIterator();
     while (iterator.hasNext()) {
@@ -579,17 +581,30 @@ public class LoadTsFileScheduler implements IScheduler {
               "Load: Successfully converted TsFile {} into tablets and 
inserted.",
               failedNode.getTsFileResource().getTsFilePath());
         } else {
-          LOGGER.warn(
-              "Load: Failed to convert to tablets from TsFile {}. Status: {}",
-              failedNode.getTsFileResource().getTsFilePath(),
-              status);
+          final String message =
+              status == null
+                  ? String.format(
+                      DataNodeQueryMessages
+                          
.EXCEPTION_FAILED_TO_CONVERT_TSFILE_ARG_TO_TABLETS_ARG_96358FDD,
+                      filePath,
+                      
DataNodeQueryMessages.MESSAGE_TABLET_CONVERSION_RETURNED_NO_STATUS_88BAB5D3)
+                  : String.format(
+                      DataNodeQueryMessages
+                          
.EXCEPTION_FAILED_TO_CONVERT_TSFILE_ARG_TO_TABLETS_STATUS_ARG_E7C9F477,
+                      filePath,
+                      status);
+          failureDetails.add(message);
+          LOGGER.warn(message);
         }
       } catch (final Exception e) {
-        LOGGER.warn(
-            "Load: Failed to convert to tablets from TsFile {}. Exception: {}",
-            failedNode.getTsFileResource().getTsFilePath(),
-            e.getMessage(),
-            e);
+        final String message =
+            String.format(
+                DataNodeQueryMessages
+                    
.EXCEPTION_FAILED_TO_CONVERT_TSFILE_ARG_TO_TABLETS_ARG_96358FDD,
+                filePath,
+                e);
+        failureDetails.add(message);
+        LOGGER.warn(message, e);
       }
     }
 
@@ -600,10 +615,10 @@ public class LoadTsFileScheduler implements IScheduler {
       stateMachine.transitionToFinished();
     } else {
       final String errorMsg =
-          "Load: failed to load some TsFiles by converting them into tablets. 
Failed TsFiles: "
-              + failedTsFileNodeIndexes.stream()
-                  .map(i -> 
tsFileNodeList.get(i).getTsFileResource().getTsFilePath())
-                  .collect(Collectors.joining(", "));
+          String.format(
+              DataNodeQueryMessages
+                  
.EXCEPTION_FAILED_TO_LOAD_TSFILES_AFTER_TABLET_CONVERSION_DETAILS_ARG_53647EC5,
+              String.join("; ", failureDetails));
       LOGGER.warn(errorMsg);
       stateMachine.transitionToFailed(new LoadFileException(errorMsg));
     }
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzerDiagnosticsTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzerDiagnosticsTest.java
new file mode 100644
index 00000000000..816b79bd013
--- /dev/null
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/analyze/load/LoadTsFileAnalyzerDiagnosticsTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.plan.analyze.load;
+
+import org.apache.iotdb.common.rpc.thrift.TEndPoint;
+import org.apache.iotdb.common.rpc.thrift.TSStatus;
+import org.apache.iotdb.db.exception.LoadAnalyzeException;
+import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
+import org.apache.iotdb.db.queryengine.common.MPPQueryContext;
+import org.apache.iotdb.db.queryengine.common.QueryId;
+import org.apache.iotdb.db.queryengine.plan.analyze.Analysis;
+import org.apache.iotdb.db.queryengine.plan.analyze.IPartitionFetcher;
+import org.apache.iotdb.db.queryengine.plan.analyze.schema.ISchemaFetcher;
+import org.apache.iotdb.db.queryengine.plan.statement.crud.LoadTsFileStatement;
+import 
org.apache.iotdb.db.storageengine.load.converter.LoadTsFileDataTypeConverter;
+import org.apache.iotdb.rpc.TSStatusCode;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+
+import java.io.File;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Optional;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@RunWith(PowerMockRunner.class)
+@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*", 
"javax.management.*"})
+@PrepareForTest(LoadTsFileAnalyzer.class)
+public class LoadTsFileAnalyzerDiagnosticsTest {
+
+  @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+  private LoadTsFileDataTypeConverter converter;
+
+  @Before
+  public void setUp() throws Exception {
+    converter = Mockito.mock(LoadTsFileDataTypeConverter.class);
+    when(converter.isSuccessful(any())).thenCallRealMethod();
+    PowerMockito.whenNew(LoadTsFileDataTypeConverter.class)
+        .withAnyArguments()
+        .thenReturn(converter);
+  }
+
+  @Test
+  public void testConversionExceptionReportsCurrentError() throws Exception {
+    when(converter.convertForTreeModel(any()))
+        .thenThrow(new IllegalStateException("tree conversion failed"));
+
+    final File file = temporaryFolder.newFile("conversion-error.tsfile");
+    final TSStatus status = convertAfterAnalysisFailure(file);
+
+    Assert.assertEquals(TSStatusCode.LOAD_FILE_ERROR.getStatusCode(), 
status.getCode());
+    Assert.assertTrue(status.getMessage().contains(file.getPath()));
+    Assert.assertTrue(status.getMessage().contains("tree conversion failed"));
+    Assert.assertFalse(status.getMessage().contains("original analysis 
failure"));
+  }
+
+  @Test
+  public void testMissingConversionStatusHasExplicitReason() throws Exception {
+    when(converter.convertForTreeModel(any())).thenReturn(Optional.empty());
+
+    final File file = temporaryFolder.newFile("no-status.tsfile");
+    final TSStatus status = convertAfterAnalysisFailure(file);
+
+    Assert.assertEquals(TSStatusCode.LOAD_FILE_ERROR.getStatusCode(), 
status.getCode());
+    Assert.assertTrue(status.getMessage().contains(file.getPath()));
+    Assert.assertTrue(
+        status
+            .getMessage()
+            
.contains(DataNodeQueryMessages.MESSAGE_TABLET_CONVERSION_RETURNED_NO_STATUS_88BAB5D3));
+  }
+
+  @Test
+  public void testConversionStatusKeepsMetadataWithoutMutatingOriginal() 
throws Exception {
+    final TSStatus original =
+        new TSStatus(TSStatusCode.MULTIPLE_ERROR.getStatusCode())
+            .setMessage("tablet insertion failed")
+            .setSubStatus(
+                Collections.singletonList(
+                    new TSStatus(TSStatusCode.NO_PERMISSION.getStatusCode())
+                        .setMessage("missing WRITE_DATA privilege")))
+            .setRedirectNode(new TEndPoint("127.0.0.2", 10730))
+            .setNeedRetry(true);
+    final TSStatus snapshot = original.deepCopy();
+    
when(converter.convertForTreeModel(any())).thenReturn(Optional.of(original));
+
+    final File file = temporaryFolder.newFile("denied.tsfile");
+    final TSStatus status = convertAfterAnalysisFailure(file);
+
+    Assert.assertNotSame(original, status);
+    Assert.assertEquals(snapshot, original);
+    Assert.assertEquals(original.getCode(), status.getCode());
+    Assert.assertEquals(original.getSubStatus(), status.getSubStatus());
+    Assert.assertEquals(original.getRedirectNode(), status.getRedirectNode());
+    Assert.assertEquals(original.isNeedRetry(), status.isNeedRetry());
+    Assert.assertTrue(status.getMessage().contains(file.getPath()));
+    Assert.assertTrue(status.getMessage().contains("missing WRITE_DATA 
privilege"));
+  }
+
+  private TSStatus convertAfterAnalysisFailure(final File file) throws 
Exception {
+    try (final LoadTsFileAnalyzer analyzer =
+        new LoadTsFileAnalyzer(
+            
LoadTsFileStatement.createUnchecked(file.getPath()).setConvertOnTypeMismatch(true),
+            new MPPQueryContext(new QueryId("load_diagnostics")),
+            Mockito.mock(IPartitionFetcher.class),
+            Mockito.mock(ISchemaFetcher.class))) {
+      final Analysis analysis = new Analysis();
+      final Method conversionMethod =
+          LoadTsFileAnalyzer.class.getDeclaredMethod(
+              "executeTabletConversionOnException", Analysis.class, 
LoadAnalyzeException.class);
+      conversionMethod.setAccessible(true);
+      conversionMethod.invoke(
+          analyzer, analysis, new LoadAnalyzeException("original analysis 
failure"));
+      Assert.assertTrue(analysis.isFinishQueryAfterAnalyze());
+      return analysis.getFailStatus();
+    }
+  }
+}
diff --git 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileDispatcherImplTest.java
 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileDispatcherImplTest.java
index b9082a54c75..b3b6653371c 100644
--- 
a/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileDispatcherImplTest.java
+++ 
b/iotdb-core/datanode/src/test/java/org/apache/iotdb/db/queryengine/plan/scheduler/load/LoadTsFileDispatcherImplTest.java
@@ -24,23 +24,27 @@ 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.sync.SyncDataNodeInternalServiceClient;
 import org.apache.iotdb.commons.consensus.DataRegionId;
 import org.apache.iotdb.commons.partition.StorageExecutor;
 import org.apache.iotdb.db.conf.IoTDBConfig;
 import org.apache.iotdb.db.conf.IoTDBDescriptor;
+import org.apache.iotdb.db.i18n.DataNodeQueryMessages;
 import org.apache.iotdb.db.queryengine.common.PlanFragmentId;
 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.planner.plan.node.PlanNodeId;
 import 
org.apache.iotdb.db.queryengine.plan.planner.plan.node.load.LoadTsFilePieceNode;
+import 
org.apache.iotdb.db.queryengine.plan.scheduler.FragInstanceDispatchResult;
 import org.apache.iotdb.db.storageengine.StorageEngine;
 import org.apache.iotdb.mpp.rpc.thrift.IDataNodeRPCService;
 import org.apache.iotdb.mpp.rpc.thrift.TLoadResp;
 import org.apache.iotdb.mpp.rpc.thrift.TTsFilePieceReq;
 import org.apache.iotdb.rpc.RpcUtils;
 import org.apache.iotdb.rpc.TElasticFramedTransport;
+import org.apache.iotdb.rpc.TSStatusCode;
 
 import org.apache.thrift.TApplicationException;
 import org.apache.thrift.TException;
@@ -246,15 +250,85 @@ public class LoadTsFileDispatcherImplTest {
         new LoadTsFilePieceNode(new PlanNodeId("piece"), new 
File("test.tsfile"));
     try (LoadTsFileDispatcherImpl dispatcher = new 
LoadTsFileDispatcherImpl(clientManager, false)) {
       dispatcher.setUuid("test-uuid");
-      Assert.assertFalse(
-          dispatcher
-              .dispatch(null, 
Collections.singletonList(createFragmentInstance(pieceNode)))
-              .get(10, TimeUnit.SECONDS)
-              .isSuccessful());
+      final FragmentInstance instance = createFragmentInstance(pieceNode);
+      final FragInstanceDispatchResult result =
+          dispatcher.dispatch(null, 
Collections.singletonList(instance)).get(10, TimeUnit.SECONDS);
+      Assert.assertFalse(result.isSuccessful());
+      Assert.assertEquals(
+          TSStatusCode.DISPATCH_ERROR.getStatusCode(), 
result.getFailureStatus().getCode());
+      
Assert.assertTrue(result.getFailureStatus().getMessage().contains("test-uuid"));
+      Assert.assertTrue(
+          result
+              .getFailureStatus()
+              .getMessage()
+              
.contains(instance.getRegionReplicaSet().getRegionId().toString()));
+      
Assert.assertTrue(result.getFailureStatus().getMessage().contains("127.0.0.1"));
     }
     Mockito.verify(client, Mockito.never()).sendTsFilePieceNode(Mockito.any());
   }
 
+  @Test
+  public void testRemoteRejectionPreservesOriginalStatus() throws Exception {
+    final TSStatus status =
+        new TSStatus(TSStatusCode.MULTIPLE_ERROR.getStatusCode())
+            .setMessage("tablet insertion rejected")
+            .setSubStatus(
+                Collections.singletonList(
+                    new TSStatus(TSStatusCode.NO_PERMISSION.getStatusCode())
+                        .setMessage("missing WRITE_DATA privilege")))
+            .setRedirectNode(new TEndPoint("127.0.0.2", 10730))
+            .setNeedRetry(true);
+
+    final FragInstanceDispatchResult result =
+        dispatchWithResponse(new TLoadResp(false).setStatus(status));
+
+    Assert.assertFalse(result.isSuccessful());
+    Assert.assertEquals(status, result.getFailureStatus());
+  }
+
+  @Test
+  public void testRemoteRejectionWithoutStatusPreservesMessage() throws 
Exception {
+    final FragInstanceDispatchResult result =
+        dispatchWithResponse(new TLoadResp(false).setMessage("receiver disk is 
full"));
+
+    Assert.assertFalse(result.isSuccessful());
+    Assert.assertEquals(
+        TSStatusCode.DISPATCH_ERROR.getStatusCode(), 
result.getFailureStatus().getCode());
+    Assert.assertEquals("receiver disk is full", 
result.getFailureStatus().getMessage());
+  }
+
+  @Test
+  public void testRemoteRejectionWithoutDetailsHasExplicitReason() throws 
Exception {
+    final FragInstanceDispatchResult result = dispatchWithResponse(new 
TLoadResp(false));
+
+    Assert.assertFalse(result.isSuccessful());
+    Assert.assertEquals(
+        TSStatusCode.DISPATCH_ERROR.getStatusCode(), 
result.getFailureStatus().getCode());
+    Assert.assertEquals(
+        DataNodeQueryMessages
+            
.MESSAGE_TARGET_NODE_REJECTED_LOAD_WITHOUT_A_STATUS_OR_A_MESSAGE_F4E4C645,
+        result.getFailureStatus().getMessage());
+  }
+
+  private static FragInstanceDispatchResult dispatchWithResponse(final 
TLoadResp response)
+      throws Exception {
+    final IClientManager<TEndPoint, SyncDataNodeInternalServiceClient> 
clientManager =
+        Mockito.mock(IClientManager.class);
+    final SyncDataNodeInternalServiceClient client =
+        Mockito.mock(SyncDataNodeInternalServiceClient.class);
+    Mockito.when(clientManager.borrowClient(Mockito.any())).thenReturn(client);
+    Mockito.when(client.getThriftMaxFrameSize()).thenReturn(4096);
+    
Mockito.when(client.sendTsFilePieceNode(Mockito.any())).thenReturn(response);
+    final LoadTsFilePieceNode pieceNode =
+        new LoadTsFilePieceNode(new PlanNodeId("piece"), new 
File("test.tsfile"));
+    try (LoadTsFileDispatcherImpl dispatcher = new 
LoadTsFileDispatcherImpl(clientManager, false)) {
+      dispatcher.setUuid("test-uuid");
+      return dispatcher
+          .dispatch(null, 
Collections.singletonList(createFragmentInstance(pieceNode)))
+          .get(10, TimeUnit.SECONDS);
+    }
+  }
+
   private static void assertFitsBothTransports(
       final TTsFilePieceReq request, final int senderMaxFrameSize, final int 
receiverMaxFrameSize)
       throws Exception {
diff --git a/pom.xml b/pom.xml
index 082fa90a23e..09ad02ff17f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1518,7 +1518,9 @@
                 -->
                 <argLine><![CDATA[
                         --add-opens=java.base/java.lang=ALL-UNNAMED
+                        --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
                         --add-opens=java.base/java.util=ALL-UNNAMED
+                        --add-opens=java.base/java.util.stream=ALL-UNNAMED
                         --add-opens=java.base/java.nio=ALL-UNNAMED
                         --add-opens=java.base/java.io=ALL-UNNAMED
                         --add-opens=java.base/java.net=ALL-UNNAMED

Reply via email to