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

sarvekshayr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git


The following commit(s) were added to refs/heads/master by this push:
     new e6dfa38dbdf HDDS-15889. Show failed MoveResult breakdown in ozone 
admin containerbalancer status -v (#10938)
e6dfa38dbdf is described below

commit e6dfa38dbdf0657a6a0d1ef701fa53c366275dce
Author: sreejasahithi <[email protected]>
AuthorDate: Tue Aug 25 13:52:53 2026 +0530

    HDDS-15889. Show failed MoveResult breakdown in ozone admin 
containerbalancer status -v (#10938)
---
 .../docs/content/feature/ContainerBalancer.md      |   4 +
 .../src/main/proto/ScmAdminProtocol.proto          |  14 ++
 .../interface-admin/src/main/resources/proto.lock  |  58 +++++
 .../container/balancer/ContainerBalancerTask.java  |  60 ++++-
 .../ContainerBalancerTaskIterationStatusInfo.java  |  42 ++++
 .../balancer/ContainerMoveFailureDetail.java       |  62 +++++
 .../balancer/ContainerMoveFailureTracker.java      |  93 +++++++
 .../scm/container/balancer/ContainerMoveInfo.java  |  13 +-
 .../TestContainerBalancerMoveFailureBreakdown.java | 278 +++++++++++++++++++++
 .../scm/cli/ContainerBalancerStatusSubcommand.java |  43 ++++
 .../datanode/TestContainerBalancerSubCommand.java  | 109 ++++++++
 11 files changed, 764 insertions(+), 12 deletions(-)

diff --git a/hadoop-hdds/docs/content/feature/ContainerBalancer.md 
b/hadoop-hdds/docs/content/feature/ContainerBalancer.md
index 848e1b998ff..94689d53223 100644
--- a/hadoop-hdds/docs/content/feature/ContainerBalancer.md
+++ b/hadoop-hdds/docs/content/feature/ContainerBalancer.md
@@ -78,6 +78,10 @@ To get a more detailed status, including the history of 
iterations:
 ozone admin containerbalancer status -v --history
 ```
 
+With `-v`/`--verbose`, when failures occur, a **Failed container moves** 
section lists each failure reason
+(e.g. `REPLICATION_FAIL_TIME_OUT`, `ITERATION_MOVE_TIMEOUT`, 
`PRE_MOVE_CONTAINER_NOT_FOUND` and more)
+with the total count and a per-datanode breakdown of which source and target 
nodes were involved.
+
 ### Stop
 
 To stop the Container Balancer:
diff --git a/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto 
b/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto
index 6c39fc22cc4..9e7244ecf82 100644
--- a/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto
+++ b/hadoop-hdds/interface-admin/src/main/proto/ScmAdminProtocol.proto
@@ -675,6 +675,20 @@ message ContainerBalancerTaskIterationStatusInfoProto {
   repeated NodeTransferInfoProto sizeEnteringNodes = 9;
   repeated NodeTransferInfoProto sizeLeavingNodes = 10;
   optional int64 iterationDuration = 11;
+  repeated ContainerMoveFailureDetailProto containerMoveFailures = 12;
+}
+
+message ContainerMoveFailureDetailProto {
+  optional string reason = 1;
+  optional int64 count = 2;
+  repeated NodeFailureCountProto sourceFailureCounts = 3;
+  repeated NodeFailureCountProto targetFailureCounts = 4;
+}
+
+message NodeFailureCountProto {
+  optional string datanodeUuid = 1;
+  optional int64 count = 2;
+  optional string hostname = 3;
 }
 
 message NodeTransferInfoProto {
diff --git a/hadoop-hdds/interface-admin/src/main/resources/proto.lock 
b/hadoop-hdds/interface-admin/src/main/resources/proto.lock
index 02184011b69..74954907009 100644
--- a/hadoop-hdds/interface-admin/src/main/resources/proto.lock
+++ b/hadoop-hdds/interface-admin/src/main/resources/proto.lock
@@ -2339,6 +2339,64 @@
                 "name": "iterationDuration",
                 "type": "int64",
                 "optional": true
+              },
+              {
+                "id": 12,
+                "name": "containerMoveFailures",
+                "type": "ContainerMoveFailureDetailProto",
+                "is_repeated": true
+              }
+            ]
+          },
+          {
+            "name": "ContainerMoveFailureDetailProto",
+            "fields": [
+              {
+                "id": 1,
+                "name": "reason",
+                "type": "string",
+                "optional": true
+              },
+              {
+                "id": 2,
+                "name": "count",
+                "type": "int64",
+                "optional": true
+              },
+              {
+                "id": 3,
+                "name": "sourceFailureCounts",
+                "type": "NodeFailureCountProto",
+                "is_repeated": true
+              },
+              {
+                "id": 4,
+                "name": "targetFailureCounts",
+                "type": "NodeFailureCountProto",
+                "is_repeated": true
+              }
+            ]
+          },
+          {
+            "name": "NodeFailureCountProto",
+            "fields": [
+              {
+                "id": 1,
+                "name": "datanodeUuid",
+                "type": "string",
+                "optional": true
+              },
+              {
+                "id": 2,
+                "name": "count",
+                "type": "int64",
+                "optional": true
+              },
+              {
+                "id": 3,
+                "name": "hostname",
+                "type": "string",
+                "optional": true
               }
             ]
           },
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerTask.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerTask.java
index c409c5eff7a..358d696d5b8 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerTask.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerTask.java
@@ -37,6 +37,7 @@
 import java.util.Objects;
 import java.util.Queue;
 import java.util.Set;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
@@ -94,6 +95,7 @@ public class ContainerBalancerTask implements Runnable {
   private Set<String> includeNodes;
   private ContainerBalancerConfiguration config;
   private ContainerBalancerMetrics metrics;
+  private final ContainerMoveFailureTracker moveFailureTracker;
   private double upperLimit;
   private double lowerLimit;
   private ContainerBalancerSelectionCriteria selectionCriteria;
@@ -152,6 +154,7 @@ public ContainerBalancerTask(StorageContainerManager scm,
     this.containerBalancer = containerBalancer;
     this.config = config;
     this.metrics = metrics;
+    this.moveFailureTracker = new ContainerMoveFailureTracker();
     this.scmContext = scm.getScmContext();
     this.overUtilizedNodes = new ArrayList<>();
     this.underUtilizedNodes = new ArrayList<>();
@@ -353,7 +356,7 @@ private ContainerBalancerTaskIterationStatusInfo 
getIterationStatistic(Integer i
         currentIterationResultName,
         iterationDuration
     );
-    ContainerMoveInfo containerMoveInfo = new ContainerMoveInfo(metrics);
+    ContainerMoveInfo containerMoveInfo = new ContainerMoveInfo(metrics, 
moveFailureTracker);
 
     DataMoveInfo dataMoveInfo =
         getDataMoveInfo(sizeEnteringDataToNodes, sizeLeavingDataFromNodes);
@@ -858,11 +861,13 @@ private long cancelMovesThatExceedTimeoutDuration() {
           CompletableFuture<MoveManager.MoveResult>>
           entry = iterator.next();
       if (!entry.getValue().isDone()) {
+        ContainerMoveSelection moveSelection = entry.getKey();
+        ContainerID containerID = moveSelection.getContainerID();
+        DatanodeDetails source = containerToSourceMap.get(containerID);
+        DatanodeDetails target = moveSelection.getTargetNode();
         LOG.warn("Container move timed out for container {} from source {}" +
-                " to target {}.", entry.getKey().getContainerID(),
-            containerToSourceMap.get(entry.getKey().getContainerID()),
-            entry.getKey().getTargetNode());
-
+                " to target {}.", containerID, source, target);
+        
moveFailureTracker.recordFailure(ContainerMoveFailureReason.ITERATION_MOVE_TIMEOUT,
 source, target);
         entry.getValue().cancel(true);
         numCancelled += 1;
       }
@@ -1011,11 +1016,15 @@ private boolean moveContainer(DatanodeDetails source,
         metrics.incrementCurrentIterationContainerMoveMetric(result, 1);
         moveSelectionToFutureMap.remove(moveSelection);
         if (ex != null) {
-          LOG.info("Container move for container {} from source {} to " +
-                  "target {} failed with exceptions.",
-              containerID, source,
-              moveSelection.getTargetNode(), ex);
-          metrics.incrementNumContainerMovesFailedInLatestIteration(1);
+          if (!isCancellationCause(ex)) {
+            LOG.info("Container move for container {} from source {} to " +
+                    "target {} failed with exceptions.",
+                containerID, source,
+                moveSelection.getTargetNode(), ex);
+            metrics.incrementNumContainerMovesFailedInLatestIteration(1);
+            
moveFailureTracker.recordFailure(MoveManager.MoveResult.FAIL_UNEXPECTED_ERROR,
+                source, moveSelection.getTargetNode());
+          }
         } else {
           if (result == MoveManager.MoveResult.COMPLETED) {
             
metrics.incrementDataSizeMovedInLatestIteration(containerInfo.getUsedBytes());
@@ -1028,6 +1037,7 @@ private boolean moveContainer(DatanodeDetails source,
                     " {} failed: {}",
                 moveSelection.getContainerID(), source,
                 moveSelection.getTargetNode(), result);
+            moveFailureTracker.recordFailure(result, source, 
moveSelection.getTargetNode());
           }
         }
       });
@@ -1039,14 +1049,20 @@ private boolean moveContainer(DatanodeDetails source,
       // exclude the permanently missing container across balancer iterations.
       
selectionCriteria.addToExcludeNotFoundContainers(moveSelection.getContainerID());
       metrics.incrementNumContainerMovesFailedInLatestIteration(1);
+      
moveFailureTracker.recordFailure(ContainerMoveFailureReason.PRE_MOVE_CONTAINER_NOT_FOUND,
+          source, moveSelection.getTargetNode());
       return false;
     } catch (NodeNotFoundException e) {
       LOG.warn("Container move failed for container {}", containerID, e);
       metrics.incrementNumContainerMovesFailedInLatestIteration(1);
+      
moveFailureTracker.recordFailure(ContainerMoveFailureReason.PRE_MOVE_NODE_NOT_FOUND,
+          source, moveSelection.getTargetNode());
       return false;
     } catch (ContainerReplicaNotFoundException e) {
       LOG.warn("Container move failed for container {}", containerID, e);
       metrics.incrementNumContainerMovesFailedInLatestIteration(1);
+      
moveFailureTracker.recordFailure(ContainerMoveFailureReason.PRE_MOVE_REPLICA_NOT_FOUND,
+          source, moveSelection.getTargetNode());
       // add source back to queue for replica not found only
       // the container is not excluded as it is a replica related failure
       findSourceStrategy.addBackSourceDataNode(source);
@@ -1233,6 +1249,11 @@ private void resetState() {
     metrics.resetDataSizeUnbalancedGB();
     metrics.resetNumDatanodesUnbalanced();
     metrics.resetNumContainerMovesFailedInLatestIteration();
+    moveFailureTracker.reset();
+  }
+
+  private static boolean isCancellationCause(Throwable ex) {
+    return ex instanceof CancellationException;
   }
 
   /**
@@ -1350,4 +1371,23 @@ enum Status {
     STOPPING,
     STOPPED
   }
+
+  /**
+   * Failure reasons recorded by {@link ContainerBalancerTask} that are not 
represented by
+   * {@link MoveManager.MoveResult}. Other move failures use {@code 
MoveResult} names in the
+   * failure breakdown.
+   * <p>
+   * {@code PRE_MOVE_*} values are recorded when {@link MoveManager#move} 
throws before returning
+   * a completed future. {@link #ITERATION_MOVE_TIMEOUT} is recorded when an 
in-flight move does
+   * not finish before the iteration move wait timeout expires.
+   */
+  enum ContainerMoveFailureReason {
+    PRE_MOVE_CONTAINER_NOT_FOUND,
+    PRE_MOVE_NODE_NOT_FOUND,
+    PRE_MOVE_REPLICA_NOT_FOUND,
+    /**
+     * Move did not complete before the iteration move wait timeout expired.
+     */
+    ITERATION_MOVE_TIMEOUT
+  }
 }
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerTaskIterationStatusInfo.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerTaskIterationStatusInfo.java
index f16f3cfe7e5..7791b6db12b 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerTaskIterationStatusInfo.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerTaskIterationStatusInfo.java
@@ -106,6 +106,14 @@ public long getContainerMovesTimeout() {
     return containerMoveInfo.getContainerMovesTimeout();
   }
 
+  /**
+   * Get per-reason failure summaries with per-datanode counts for this 
iteration.
+   * @return list of failure details, one entry per failure reason
+   */
+  public List<ContainerMoveFailureDetail> getFailures() {
+    return containerMoveInfo.getFailures();
+  }
+
   /**
    * Get a map of the node IDs and the corresponding data sizes moved to each 
node.
    * @return nodeId to size entering from node map
@@ -151,9 +159,43 @@ public 
StorageContainerLocationProtocolProtos.ContainerBalancerTaskIterationStat
         .addAllSizeLeavingNodes(
             mapToProtoNodeTransferInfo(getSizeLeavingNodes())
         )
+        .addAllContainerMoveFailures(mapToProtoFailures(getFailures()))
         .build();
   }
 
+  private 
List<StorageContainerLocationProtocolProtos.ContainerMoveFailureDetailProto> 
mapToProtoFailures(
+      List<ContainerMoveFailureDetail> failures) {
+    return failures.stream()
+        .map(failure -> {
+          Map<String, String> datanodeHostnames = 
failure.getDatanodeHostnames();
+          
StorageContainerLocationProtocolProtos.ContainerMoveFailureDetailProto.Builder 
builder =
+              
StorageContainerLocationProtocolProtos.ContainerMoveFailureDetailProto.newBuilder()
+                  .setReason(failure.getReason())
+                  .setCount(failure.getCount());
+          failure.getSourceFailureCounts().forEach((uuid, count) ->
+              builder.addSourceFailureCounts(
+                  toNodeFailureCountProto(uuid, count, datanodeHostnames)));
+          failure.getTargetFailureCounts().forEach((uuid, count) ->
+              builder.addTargetFailureCounts(
+                  toNodeFailureCountProto(uuid, count, datanodeHostnames)));
+          return builder.build();
+        })
+        .collect(Collectors.toList());
+  }
+
+  private StorageContainerLocationProtocolProtos.NodeFailureCountProto 
toNodeFailureCountProto(
+      String uuid, long count, Map<String, String> datanodeHostnames) {
+    StorageContainerLocationProtocolProtos.NodeFailureCountProto.Builder 
builder =
+        
StorageContainerLocationProtocolProtos.NodeFailureCountProto.newBuilder()
+            .setDatanodeUuid(uuid)
+            .setCount(count);
+    String hostname = datanodeHostnames.get(uuid);
+    if (hostname != null && !hostname.isEmpty()) {
+      builder.setHostname(hostname);
+    }
+    return builder.build();
+  }
+
   /**
    * Converts an instance into the protobuf compatible object.
    * @param nodes node id to node traffic size
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerMoveFailureDetail.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerMoveFailureDetail.java
new file mode 100644
index 00000000000..a25f44a4f76
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerMoveFailureDetail.java
@@ -0,0 +1,62 @@
+/*
+ * 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.hadoop.hdds.scm.container.balancer;
+
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Per-reason container move failure summary with per-datanode failure counts.
+ */
+public final class ContainerMoveFailureDetail {
+  private final String reason;
+  private final long count;
+  private final Map<String, Long> sourceFailureCounts;
+  private final Map<String, Long> targetFailureCounts;
+  private final Map<String, String> datanodeHostnames;
+
+  public ContainerMoveFailureDetail(String reason, long count,
+      Map<String, Long> sourceFailureCounts, Map<String, Long> 
targetFailureCounts,
+      Map<String, String> datanodeHostnames) {
+    this.reason = reason;
+    this.count = count;
+    this.sourceFailureCounts = 
Collections.unmodifiableMap(sourceFailureCounts);
+    this.targetFailureCounts = 
Collections.unmodifiableMap(targetFailureCounts);
+    this.datanodeHostnames = Collections.unmodifiableMap(datanodeHostnames);
+  }
+
+  public String getReason() {
+    return reason;
+  }
+
+  public long getCount() {
+    return count;
+  }
+
+  public Map<String, Long> getSourceFailureCounts() {
+    return sourceFailureCounts;
+  }
+
+  public Map<String, Long> getTargetFailureCounts() {
+    return targetFailureCounts;
+  }
+
+  public Map<String, String> getDatanodeHostnames() {
+    return datanodeHostnames;
+  }
+}
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerMoveFailureTracker.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerMoveFailureTracker.java
new file mode 100644
index 00000000000..ac8d712ba4b
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerMoveFailureTracker.java
@@ -0,0 +1,93 @@
+/*
+ * 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.hadoop.hdds.scm.container.balancer;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+
+/**
+ * Tracks per-iteration container move failures by reason and per-datanode 
counts.
+ */
+final class ContainerMoveFailureTracker {
+  private final Map<String, Long> failuresByReason = new HashMap<>();
+  private final Map<String, Map<String, Long>> sourceFailureCountsByReason = 
new HashMap<>();
+  private final Map<String, Map<String, Long>> targetFailureCountsByReason = 
new HashMap<>();
+  private final Map<String, String> datanodeHostnames = new HashMap<>();
+
+  synchronized void recordFailure(MoveManager.MoveResult result, 
DatanodeDetails source,
+                                         DatanodeDetails target) {
+    recordFailure(result.name(), source, target);
+  }
+
+  synchronized void 
recordFailure(ContainerBalancerTask.ContainerMoveFailureReason result,
+                                         DatanodeDetails source, 
DatanodeDetails target) {
+    recordFailure(result.name(), source, target);
+  }
+
+  synchronized void recordFailure(String reason, DatanodeDetails source, 
DatanodeDetails target) {
+    failuresByReason.merge(reason, 1L, Long::sum);
+    if (source != null) {
+      datanodeHostnames.putIfAbsent(source.getUuidString(), 
source.getHostName());
+      sourceFailureCountsByReason.computeIfAbsent(reason, k -> new HashMap<>())
+          .merge(source.getUuidString(), 1L, Long::sum);
+    }
+    if (target != null) {
+      datanodeHostnames.putIfAbsent(target.getUuidString(), 
target.getHostName());
+      targetFailureCountsByReason.computeIfAbsent(reason, k -> new HashMap<>())
+          .merge(target.getUuidString(), 1L, Long::sum);
+    }
+  }
+
+  synchronized void reset() {
+    failuresByReason.clear();
+    sourceFailureCountsByReason.clear();
+    targetFailureCountsByReason.clear();
+    datanodeHostnames.clear();
+  }
+
+  synchronized List<ContainerMoveFailureDetail> getFailures() {
+    List<ContainerMoveFailureDetail> result = new ArrayList<>();
+    for (Map.Entry<String, Long> entry : failuresByReason.entrySet()) {
+      String reason = entry.getKey();
+      long count = entry.getValue();
+      Map<String, Long> srcCounts = 
sourceFailureCountsByReason.getOrDefault(reason, Collections.emptyMap());
+      Map<String, Long> tgtCounts = 
targetFailureCountsByReason.getOrDefault(reason, Collections.emptyMap());
+      result.add(new ContainerMoveFailureDetail(reason, count, new 
HashMap<>(srcCounts), new HashMap<>(tgtCounts),
+          copyHostnamesForDetail(srcCounts, tgtCounts)));
+    }
+    return result;
+  }
+
+  private Map<String, String> copyHostnamesForDetail(Map<String, Long> 
srcCounts, Map<String, Long> tgtCounts) {
+    Map<String, String> hostnames = new HashMap<>();
+    srcCounts.keySet().forEach(uuid -> copyHostnameIfPresent(uuid, hostnames));
+    tgtCounts.keySet().forEach(uuid -> copyHostnameIfPresent(uuid, hostnames));
+    return hostnames;
+  }
+
+  private void copyHostnameIfPresent(String uuid, Map<String, String> 
hostnames) {
+    String hostname = datanodeHostnames.get(uuid);
+    if (hostname != null && !hostname.isEmpty()) {
+      hostnames.put(uuid, hostname);
+    }
+  }
+}
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerMoveInfo.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerMoveInfo.java
index 3b3c7f91933..2dab77120dd 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerMoveInfo.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerMoveInfo.java
@@ -17,6 +17,8 @@
 
 package org.apache.hadoop.hdds.scm.container.balancer;
 
+import java.util.List;
+
 /**
  * Information about moving containers.
  */
@@ -25,20 +27,23 @@ public class ContainerMoveInfo {
   private final long containerMovesCompleted;
   private final long containerMovesFailed;
   private final long containerMovesTimeout;
+  private final List<ContainerMoveFailureDetail> failures;
 
   public ContainerMoveInfo(long containerMovesScheduled, long 
containerMovesCompleted, long containerMovesFailed,
-                           long containerMovesTimeout) {
+                           long containerMovesTimeout, 
List<ContainerMoveFailureDetail> failures) {
     this.containerMovesScheduled = containerMovesScheduled;
     this.containerMovesCompleted = containerMovesCompleted;
     this.containerMovesFailed = containerMovesFailed;
     this.containerMovesTimeout = containerMovesTimeout;
+    this.failures = failures;
   }
 
-  public ContainerMoveInfo(ContainerBalancerMetrics metrics) {
+  public ContainerMoveInfo(ContainerBalancerMetrics metrics, 
ContainerMoveFailureTracker failureTracker) {
     this.containerMovesScheduled = 
metrics.getNumContainerMovesScheduledInLatestIteration();
     this.containerMovesCompleted = 
metrics.getNumContainerMovesCompletedInLatestIteration();
     this.containerMovesFailed = 
metrics.getNumContainerMovesFailedInLatestIteration();
     this.containerMovesTimeout = 
metrics.getNumContainerMovesTimeoutInLatestIteration();
+    this.failures = failureTracker.getFailures();
   }
 
   public long getContainerMovesScheduled() {
@@ -56,4 +61,8 @@ public long getContainerMovesFailed() {
   public long getContainerMovesTimeout() {
     return containerMovesTimeout;
   }
+
+  public List<ContainerMoveFailureDetail> getFailures() {
+    return failures;
+  }
 }
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancerMoveFailureBreakdown.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancerMoveFailureBreakdown.java
new file mode 100644
index 00000000000..edc159f07a4
--- /dev/null
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancerMoveFailureBreakdown.java
@@ -0,0 +1,278 @@
+/*
+ * 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.hadoop.hdds.scm.container.balancer;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.atLeast;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.MockDatanodeDetails;
+import org.apache.hadoop.hdds.scm.container.ContainerID;
+import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException;
+import org.apache.hadoop.hdds.scm.container.ContainerReplicaNotFoundException;
+import org.apache.hadoop.hdds.scm.node.states.NodeNotFoundException;
+import org.apache.hadoop.ozone.OzoneConsts;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+/**
+ * Tests that {@link ContainerBalancerTask} records failure breakdown and 
details
+ * in iteration statistics for common move failure reasons.
+ */
+class TestContainerBalancerMoveFailureBreakdown {
+
+  private static final int NODE_COUNT = 5;
+  private static final long STORAGE_UNIT = OzoneConsts.GB;
+
+  @Test
+  void testReplicationTimeoutRecordedInFailureBreakdown()
+      throws NodeNotFoundException, ContainerReplicaNotFoundException, 
ContainerNotFoundException {
+    MockedSCM mockedScm = createMockedScm();
+    mockMoveFailureOnce(mockedScm, 
MoveManager.MoveResult.REPLICATION_FAIL_TIME_OUT);
+
+    ContainerBalancerTask task = 
mockedScm.startBalancerTask(buildConfig(mockedScm));
+    ContainerBalancerTaskIterationStatusInfo iteration = 
getCompletedIteration(task);
+
+    assertThat(iteration.getContainerMovesTimeout()).isEqualTo(1);
+    assertFailureBreakdown(mockedScm, iteration, 
MoveManager.MoveResult.REPLICATION_FAIL_TIME_OUT.name(), 0);
+  }
+
+  @Test
+  void testReplicationNotHealthyAfterMoveRecordedInFailureBreakdown()
+      throws NodeNotFoundException, ContainerReplicaNotFoundException, 
ContainerNotFoundException {
+    MockedSCM mockedScm = createMockedScm();
+    mockMoveFailureOnce(mockedScm, 
MoveManager.MoveResult.REPLICATION_NOT_HEALTHY_AFTER_MOVE);
+
+    ContainerBalancerTask task = 
mockedScm.startBalancerTask(buildConfig(mockedScm));
+    ContainerBalancerTaskIterationStatusInfo iteration = 
getCompletedIteration(task);
+
+    assertThat(iteration.getContainerMovesFailed()).isEqualTo(1);
+    assertFailureBreakdown(mockedScm, iteration,
+            MoveManager.MoveResult.REPLICATION_NOT_HEALTHY_AFTER_MOVE.name(), 
0);
+  }
+
+  @Test
+  void testDeletionTimeoutRecordedInFailureBreakdown()
+      throws NodeNotFoundException, ContainerReplicaNotFoundException, 
ContainerNotFoundException {
+    MockedSCM mockedScm = createMockedScm();
+    mockMoveFailureOnce(mockedScm, 
MoveManager.MoveResult.DELETION_FAIL_TIME_OUT);
+
+    ContainerBalancerTask task = 
mockedScm.startBalancerTask(buildConfig(mockedScm));
+    ContainerBalancerTaskIterationStatusInfo iteration = 
getCompletedIteration(task);
+
+    assertThat(iteration.getContainerMovesTimeout()).isEqualTo(1);
+    assertFailureBreakdown(mockedScm, iteration, 
MoveManager.MoveResult.DELETION_FAIL_TIME_OUT.name(), 0);
+  }
+
+  @Test
+  void testIterationMoveTimeoutRecordedInFailureBreakdown()
+      throws NodeNotFoundException, ContainerNotFoundException, 
ContainerReplicaNotFoundException {
+    MockedSCM mockedScm = createMockedScm();
+    ContainerBalancerConfiguration config = buildConfig(mockedScm);
+    config.setMoveTimeout(Duration.ofMillis(50));
+    mockFirstMoveNeverCompletes(mockedScm);
+
+    ContainerBalancerTask task = mockedScm.startBalancerTask(config);
+    ContainerBalancerTaskIterationStatusInfo iteration = 
getCompletedIteration(task);
+
+    assertThat(iteration.getContainerMovesTimeout()).isEqualTo(1);
+    assertFailureBreakdown(mockedScm, iteration,
+            
ContainerBalancerTask.ContainerMoveFailureReason.ITERATION_MOVE_TIMEOUT.name(), 
0);
+  }
+
+  @Test
+  void testFailureBreakdownTotalsMatchHeadlineCountersInControlledScenario()
+      throws NodeNotFoundException, ContainerReplicaNotFoundException, 
ContainerNotFoundException {
+    MockedSCM mockedScm = createMockedScm();
+    when(mockedScm.getMoveManager().move(any(ContainerID.class),
+        any(DatanodeDetails.class), any(DatanodeDetails.class)))
+        .thenReturn(CompletableFuture.completedFuture(
+            MoveManager.MoveResult.REPLICATION_FAIL_TIME_OUT))
+        .thenReturn(CompletableFuture.completedFuture(
+            MoveManager.MoveResult.REPLICATION_NOT_HEALTHY_AFTER_MOVE))
+        
.thenReturn(CompletableFuture.completedFuture(MoveManager.MoveResult.COMPLETED));
+
+    ContainerBalancerTask task = 
mockedScm.startBalancerTask(buildConfig(mockedScm));
+    ContainerBalancerTaskIterationStatusInfo iteration = 
getCompletedIteration(task);
+
+    assertThat(iteration.getContainerMovesTimeout()).isEqualTo(1);
+    assertThat(iteration.getContainerMovesFailed()).isEqualTo(1);
+    verify(mockedScm.getMoveManager(), atLeast(2)).move(
+            any(ContainerID.class), any(DatanodeDetails.class), 
any(DatanodeDetails.class));
+    assertBreakdownTotalsMatchHeadlineCounters(iteration);
+  }
+
+  @Test
+  void testPreMoveContainerNotFoundRecordedInFailureBreakdown()
+      throws NodeNotFoundException, ContainerReplicaNotFoundException, 
ContainerNotFoundException {
+    MockedSCM mockedScm = createMockedScm();
+    when(mockedScm.getMoveManager().move(any(ContainerID.class),
+        any(DatanodeDetails.class), any(DatanodeDetails.class)))
+        .thenThrow(ContainerNotFoundException.newInstanceForTesting())
+        
.thenReturn(CompletableFuture.completedFuture(MoveManager.MoveResult.COMPLETED));
+
+    ContainerBalancerTask task = 
mockedScm.startBalancerTask(buildConfig(mockedScm));
+    ContainerBalancerTaskIterationStatusInfo iteration = 
getCompletedIteration(task);
+
+    assertThat(iteration.getContainerMovesFailed()).isEqualTo(1);
+    assertFailureBreakdown(mockedScm, iteration,
+            
ContainerBalancerTask.ContainerMoveFailureReason.PRE_MOVE_CONTAINER_NOT_FOUND.name(),
 0);
+  }
+
+  @Test
+  void testNodeNotFoundRecordedInFailureBreakdown()
+      throws NodeNotFoundException, ContainerReplicaNotFoundException, 
ContainerNotFoundException {
+    MockedSCM mockedScm = createMockedScm();
+    when(mockedScm.getMoveManager().move(any(ContainerID.class),
+        any(DatanodeDetails.class), any(DatanodeDetails.class)))
+        .thenThrow(new NodeNotFoundException())
+        
.thenReturn(CompletableFuture.completedFuture(MoveManager.MoveResult.COMPLETED));
+    ContainerBalancerTask task = 
mockedScm.startBalancerTask(buildConfig(mockedScm));
+    ContainerBalancerTaskIterationStatusInfo iteration = 
getCompletedIteration(task);
+    assertThat(iteration.getContainerMovesFailed()).isEqualTo(1);
+    assertFailureBreakdown(mockedScm, iteration,
+        
ContainerBalancerTask.ContainerMoveFailureReason.PRE_MOVE_NODE_NOT_FOUND.name(),
 0);
+  }
+
+  @Test
+  void testReplicaNotFoundRecordedInFailureBreakdown()
+      throws NodeNotFoundException, ContainerReplicaNotFoundException, 
ContainerNotFoundException {
+    MockedSCM mockedScm = createMockedScm();
+    when(mockedScm.getMoveManager().move(any(ContainerID.class),
+        any(DatanodeDetails.class), any(DatanodeDetails.class)))
+        .thenThrow(new ContainerReplicaNotFoundException("test"))
+        
.thenReturn(CompletableFuture.completedFuture(MoveManager.MoveResult.COMPLETED));
+    ContainerBalancerTask task = 
mockedScm.startBalancerTask(buildConfig(mockedScm));
+    ContainerBalancerTaskIterationStatusInfo iteration = 
getCompletedIteration(task);
+    assertThat(iteration.getContainerMovesFailed()).isEqualTo(1);
+    assertFailureBreakdown(mockedScm, iteration,
+        
ContainerBalancerTask.ContainerMoveFailureReason.PRE_MOVE_REPLICA_NOT_FOUND.name(),
 0);
+  }
+
+  @Test
+  void testSameReasonAggregatesSourceFailureCountsFromMultipleSources() {
+    ContainerMoveFailureTracker tracker = new ContainerMoveFailureTracker();
+    DatanodeDetails source1 = 
MockDatanodeDetails.createDatanodeDetails("1.1.1.1", "/r1");
+    DatanodeDetails source2 = 
MockDatanodeDetails.createDatanodeDetails("2.2.2.2", "/r1");
+    DatanodeDetails target = 
MockDatanodeDetails.createDatanodeDetails("3.3.3.3", "/r2");
+
+    String reason = MoveManager.MoveResult.REPLICATION_FAIL_TIME_OUT.name();
+    tracker.recordFailure(reason, source1, target);
+    tracker.recordFailure(reason, source2, target);
+
+    ContainerMoveFailureDetail detail = tracker.getFailures().stream()
+        .filter(f -> reason.equals(f.getReason()))
+        .findFirst()
+        .orElse(null);
+    assertThat(detail).as("failure detail for reason " + reason).isNotNull();
+    assertThat(detail.getCount()).isEqualTo(2L);
+    assertThat(detail.getSourceFailureCounts())
+        .hasSize(2)
+        .containsEntry(source1.getUuidString(), 1L)
+        .containsEntry(source2.getUuidString(), 1L);
+    assertThat(detail.getTargetFailureCounts())
+        .hasSize(1)
+        .containsEntry(target.getUuidString(), 2L);
+  }
+
+  private static MockedSCM createMockedScm() {
+    return new MockedSCM(new MockCluster(NODE_COUNT, STORAGE_UNIT));
+  }
+
+  private static void mockMoveFailureOnce(MockedSCM mockedScm, 
MoveManager.MoveResult failureResult)
+      throws NodeNotFoundException, ContainerReplicaNotFoundException, 
ContainerNotFoundException {
+    when(mockedScm.getMoveManager().move(any(ContainerID.class),
+        any(DatanodeDetails.class), any(DatanodeDetails.class)))
+        .thenReturn(CompletableFuture.completedFuture(failureResult))
+        
.thenReturn(CompletableFuture.completedFuture(MoveManager.MoveResult.COMPLETED));
+  }
+
+  private static void mockFirstMoveNeverCompletes(MockedSCM mockedScm)
+          throws NodeNotFoundException, ContainerReplicaNotFoundException, 
ContainerNotFoundException {
+    AtomicInteger moveInvocations = new AtomicInteger(0);
+    when(mockedScm.getMoveManager().move(any(ContainerID.class),
+            any(DatanodeDetails.class), any(DatanodeDetails.class)))
+            .thenAnswer(invocation -> {
+              if (moveInvocations.getAndIncrement() == 0) {
+                return new CompletableFuture<>();
+              }
+              return 
CompletableFuture.completedFuture(MoveManager.MoveResult.COMPLETED);
+            });
+  }
+
+  private static ContainerBalancerConfiguration buildConfig(MockedSCM 
mockedScm) {
+    ContainerBalancerConfiguration config = new 
ContainerBalancerConfigBuilder(mockedScm.getNodeCount()).build();
+    config.setMaxSizeToMovePerIteration(5 * STORAGE_UNIT);
+    config.setMaxSizeEnteringTarget(5 * STORAGE_UNIT);
+    config.setMaxDatanodesPercentageToInvolvePerIteration(100);
+    return config;
+  }
+
+  private static ContainerBalancerTaskIterationStatusInfo 
getCompletedIteration(
+      ContainerBalancerTask task) {
+    List<ContainerBalancerTaskIterationStatusInfo> iterations =
+        task.getCurrentIterationsStatistic();
+    assertEquals(1, iterations.size());
+    ContainerBalancerTaskIterationStatusInfo iteration = iterations.get(0);
+    assertEquals("ITERATION_COMPLETED", iteration.getIterationResult());
+    return iteration;
+  }
+
+  private static void assertBreakdownTotalsMatchHeadlineCounters(
+      ContainerBalancerTaskIterationStatusInfo iteration) {
+    long breakdownTotal = iteration.getFailures().stream()
+        .mapToLong(ContainerMoveFailureDetail::getCount)
+        .sum();
+    long headlineTotal = iteration.getContainerMovesFailed() + 
iteration.getContainerMovesTimeout();
+    assertEquals(headlineTotal, breakdownTotal,
+        "sum(failure breakdown) should equal failed + timeout counters");
+  }
+
+  private static void assertFailureBreakdown(MockedSCM mockedScm, 
ContainerBalancerTaskIterationStatusInfo iteration,
+      String expectedReason, int moveIndex) throws NodeNotFoundException, 
ContainerReplicaNotFoundException,
+      ContainerNotFoundException {
+    ArgumentCaptor<DatanodeDetails> sourceCaptor = 
ArgumentCaptor.forClass(DatanodeDetails.class);
+    ArgumentCaptor<DatanodeDetails> targetCaptor = 
ArgumentCaptor.forClass(DatanodeDetails.class);
+    verify(mockedScm.getMoveManager(), atLeastOnce()).move(
+            any(ContainerID.class), sourceCaptor.capture(), 
targetCaptor.capture());
+    assertThat(sourceCaptor.getAllValues().size()).isGreaterThan(moveIndex);
+    assertThat(targetCaptor.getAllValues().size()).isGreaterThan(moveIndex);
+    String sourceUuid = 
sourceCaptor.getAllValues().get(moveIndex).getUuidString();
+    String targetUuid = 
targetCaptor.getAllValues().get(moveIndex).getUuidString();
+
+    List<ContainerMoveFailureDetail> failures = iteration.getFailures();
+    assertThat(failures).as("failure details").isNotEmpty();
+    ContainerMoveFailureDetail detail = failures.stream()
+        .filter(f -> expectedReason.equals(f.getReason()))
+        .findFirst()
+        .orElse(null);
+    assertThat(detail).as("failure detail for reason " + 
expectedReason).isNotNull();
+    assertThat(detail.getCount()).isEqualTo(1L);
+    
assertThat(detail.getSourceFailureCounts()).hasSize(1).containsEntry(sourceUuid,
 1L);
+    
assertThat(detail.getTargetFailureCounts()).hasSize(1).containsEntry(targetUuid,
 1L);
+  }
+}
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerStatusSubcommand.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerStatusSubcommand.java
index a55fc7906db..1839c3915fb 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerStatusSubcommand.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerStatusSubcommand.java
@@ -27,6 +27,7 @@
 import java.time.OffsetDateTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
+import java.util.Comparator;
 import java.util.List;
 import java.util.stream.Collectors;
 import org.apache.hadoop.hdds.cli.HddsVersionProvider;
@@ -34,6 +35,8 @@
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ContainerBalancerStatusInfoProto;
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ContainerBalancerStatusInfoResponseProto;
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ContainerBalancerTaskIterationStatusInfoProto;
+import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.ContainerMoveFailureDetailProto;
+import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerLocationProtocolProtos.NodeFailureCountProto;
 import org.apache.hadoop.hdds.scm.client.ScmClient;
 import org.apache.hadoop.ozone.OzoneConsts;
 import picocli.CommandLine;
@@ -232,6 +235,7 @@ private String 
getPrettyIterationStatusInfo(ContainerBalancerTaskIterationStatus
     if (leavingDataNodeList.isEmpty()) {
       leavingDataNodeList = " -" + System.lineSeparator();
     }
+    String failures = formatFailures(containerMovesFailed, 
iterationStatusInfo.getContainerMoveFailuresList());
     return String.format(
             "%-50s %s%n" +
                     "%-50s %s%n" +
@@ -243,6 +247,7 @@ private String 
getPrettyIterationStatusInfo(ContainerBalancerTaskIterationStatus
                     "%-50s %s%n" +
                     "%-50s %s%n" +
                     "%-50s %s%n" +
+                    "%s" +
                     "%-50s %n%s" +
                     "%-50s %n%s",
             "Key", "Value",
@@ -256,9 +261,47 @@ private String 
getPrettyIterationStatusInfo(ContainerBalancerTaskIterationStatus
             "Already moved containers", containerMovesCompleted,
             "Failed to move containers", containerMovesFailed,
             "Failed to move containers by timeout", containerMovesTimeout,
+            failures,
             "Entered data to nodes", enteringDataNodeList,
             "Exited data from nodes", leavingDataNodeList);
   }
 
+  private String formatFailures(long containerMovesFailed, 
List<ContainerMoveFailureDetailProto> failures) {
+    if (containerMovesFailed > 0 && failures.isEmpty()) {
+      return String.format("%-50s %s%n", "Failed container moves", "(no 
breakdown available)");
+    }
+    if (failures.isEmpty()) {
+      return "";
+    }
+    List<ContainerMoveFailureDetailProto> sorted = failures.stream()
+        
.sorted(Comparator.comparingLong(ContainerMoveFailureDetailProto::getCount).reversed()
+            .thenComparing(ContainerMoveFailureDetailProto::getReason))
+        .collect(Collectors.toList());
+    StringBuilder builder = new StringBuilder();
+    builder.append(String.format("%-50s %n", "Failed container moves"));
+    for (ContainerMoveFailureDetailProto failure : sorted) {
+      builder.append(String.format("  %-48s %d%n", failure.getReason(), 
failure.getCount()));
+      if (!failure.getSourceFailureCountsList().isEmpty()) {
+        builder.append(String.format("    %-46s %n", "Source datanodes"));
+        for (NodeFailureCountProto src : failure.getSourceFailureCountsList()) 
{
+          builder.append(String.format("      %-44s %d%n", 
formatDatanodeLabel(src), src.getCount()));
+        }
+      }
+      if (!failure.getTargetFailureCountsList().isEmpty()) {
+        builder.append(String.format("    %-46s %n", "Target datanodes"));
+        for (NodeFailureCountProto tgt : failure.getTargetFailureCountsList()) 
{
+          builder.append(String.format("      %-44s %d%n", 
formatDatanodeLabel(tgt), tgt.getCount()));
+        }
+      }
+    }
+    return builder.toString();
+  }
+
+  private static String formatDatanodeLabel(NodeFailureCountProto node) {
+    return node.hasHostname()
+        ? node.getHostname() + " (" + node.getDatanodeUuid() + ")"
+        : node.getDatanodeUuid();
+  }
+
 }
 
diff --git 
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestContainerBalancerSubCommand.java
 
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestContainerBalancerSubCommand.java
index 407785d48a3..5eca86a8a6c 100644
--- 
a/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestContainerBalancerSubCommand.java
+++ 
b/hadoop-ozone/cli-admin/src/test/java/org/apache/hadoop/hdds/scm/cli/datanode/TestContainerBalancerSubCommand.java
@@ -712,4 +712,113 @@ void 
testContainerBalancerStatusSubcommandStoppedAfterAllIterationsCompleteVerbo
         .contains(ITERATION_2_COMPLETED_OUTPUT)
         .doesNotContain(ITERATION_3_COMPLETED_OUTPUT);
   }
+
+  @Test
+  void testContainerBalancerStatusVerboseShowsFailureBreakdown() throws 
IOException {
+    ScmClient scmClient = mock(ScmClient.class);
+    ContainerBalancerConfiguration config = 
getContainerBalancerConfiguration();
+    
StorageContainerLocationProtocolProtos.ContainerBalancerTaskIterationStatusInfoProto
 iteration =
+        
StorageContainerLocationProtocolProtos.ContainerBalancerTaskIterationStatusInfoProto.newBuilder()
+            .setIterationNumber(1)
+            .setIterationResult("ITERATION_COMPLETED")
+            .setIterationDuration(120L)
+            .setContainerMovesScheduled(5)
+            .setContainerMovesCompleted(3)
+            .setContainerMovesFailed(1)
+            .setContainerMovesTimeout(2)
+            .addContainerMoveFailures(
+                
StorageContainerLocationProtocolProtos.ContainerMoveFailureDetailProto.newBuilder()
+                    .setReason("REPLICATION_FAIL_TIME_OUT")
+                    .setCount(2)
+                    .addSourceFailureCounts(
+                        
StorageContainerLocationProtocolProtos.NodeFailureCountProto.newBuilder()
+                            
.setDatanodeUuid("source-uuid-1").setHostname("datanode1.example.com").setCount(1).build())
+                    .addSourceFailureCounts(
+                        
StorageContainerLocationProtocolProtos.NodeFailureCountProto.newBuilder()
+                            
.setDatanodeUuid("source-uuid-2").setCount(1).build())
+                    .addTargetFailureCounts(
+                        
StorageContainerLocationProtocolProtos.NodeFailureCountProto.newBuilder()
+                            
.setDatanodeUuid("target-uuid-1").setHostname("datanode3.example.com").setCount(1).build())
+                    .addTargetFailureCounts(
+                        
StorageContainerLocationProtocolProtos.NodeFailureCountProto.newBuilder()
+                            
.setDatanodeUuid("target-uuid-2").setHostname("datanode4.example.com").setCount(1).build())
+                    .build())
+            .addContainerMoveFailures(
+                
StorageContainerLocationProtocolProtos.ContainerMoveFailureDetailProto.newBuilder()
+                    .setReason("PRE_MOVE_CONTAINER_NOT_FOUND")
+                    .setCount(1)
+                    .addSourceFailureCounts(
+                        
StorageContainerLocationProtocolProtos.NodeFailureCountProto.newBuilder()
+                            
.setDatanodeUuid("source-uuid-3").setHostname("datanode5.example.com").setCount(1).build())
+                    .addTargetFailureCounts(
+                        
StorageContainerLocationProtocolProtos.NodeFailureCountProto.newBuilder()
+                            
.setDatanodeUuid("target-uuid-3").setHostname("datanode6.example.com").setCount(1).build())
+                    .build())
+            .build();
+
+    long stoppedAt = OffsetDateTime.now().toEpochSecond();
+    ContainerBalancerStatusInfoProto statusInfo = 
ContainerBalancerStatusInfoProto.newBuilder()
+        .setStartedAt(stoppedAt - 600)
+        .setStoppedAt(stoppedAt)
+        .setStopReason("USER_REQUESTED")
+        .setStopMessage("Stopped by user request.")
+        .setConfiguration(config.toProtobufBuilder().setShouldRun(false))
+        .addIterationsStatusInfo(iteration)
+        .build();
+
+    when(scmClient.getContainerBalancerStatusInfo())
+        .thenReturn(ContainerBalancerStatusInfoResponseProto.newBuilder()
+            .setIsRunning(false)
+            .setContainerBalancerStatusInfo(statusInfo)
+            .build());
+
+    verbose.set(true);
+    statusCmd.execute(scmClient);
+
+    assertThat(out.get())
+        .contains("Failed container moves")
+        .contains("REPLICATION_FAIL_TIME_OUT")
+        .contains("PRE_MOVE_CONTAINER_NOT_FOUND")
+        .contains("datanode1.example.com (source-uuid-1)")
+        .contains("source-uuid-2")
+        .doesNotContain("(source-uuid-2)")
+        .contains("datanode3.example.com (target-uuid-1)")
+        .contains("datanode5.example.com (source-uuid-3)")
+        .contains("datanode6.example.com (target-uuid-3)");
+  }
+
+  @Test
+  void testContainerBalancerStatusVerboseShowsNoBreakdownWhenFailuresMissing() 
throws IOException {
+    ScmClient scmClient = mock(ScmClient.class);
+    ContainerBalancerConfiguration config = 
getContainerBalancerConfiguration();
+    
StorageContainerLocationProtocolProtos.ContainerBalancerTaskIterationStatusInfoProto
 iteration =
+        
StorageContainerLocationProtocolProtos.ContainerBalancerTaskIterationStatusInfoProto.newBuilder()
+            .setIterationNumber(1)
+            .setIterationResult("ITERATION_COMPLETED")
+            .setIterationDuration(120L)
+            .setContainerMovesFailed(3)
+            .build();
+
+    long stoppedAt = OffsetDateTime.now().toEpochSecond();
+    ContainerBalancerStatusInfoProto statusInfo = 
ContainerBalancerStatusInfoProto.newBuilder()
+        .setStartedAt(stoppedAt - 600)
+        .setStoppedAt(stoppedAt)
+        .setStopReason("USER_REQUESTED")
+        .setConfiguration(config.toProtobufBuilder().setShouldRun(false))
+        .addIterationsStatusInfo(iteration)
+        .build();
+
+    when(scmClient.getContainerBalancerStatusInfo())
+        .thenReturn(ContainerBalancerStatusInfoResponseProto.newBuilder()
+            .setIsRunning(false)
+            .setContainerBalancerStatusInfo(statusInfo)
+            .build());
+
+    verbose.set(true);
+    statusCmd.execute(scmClient);
+
+    assertThat(out.get())
+        .contains("Failed to move containers                          3")
+        .contains("Failed container moves                             (no 
breakdown available)");
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to