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 4250410bc29 HDDS-16173. Add cluster utilization analysis for container 
balancer CLI commands (#11037)
4250410bc29 is described below

commit 4250410bc291ea4ca26d207fb2065b5493fb1ec7
Author: sravani <[email protected]>
AuthorDate: Thu Sep 17 12:57:20 2026 +0530

    HDDS-16173. Add cluster utilization analysis for container balancer CLI 
commands (#11037)
---
 .../balancer/ContainerBalancerClusterAnalyzer.java | 272 +++++++++++++++++++
 .../balancer/ContainerBalancerClusterSnapshot.java | 250 +++++++++++++++++
 .../TestContainerBalancerClusterAnalyzer.java      | 296 +++++++++++++++++++++
 .../scm/container/balancer/ContainerBalancer.java  |  10 +-
 .../container/balancer/ContainerBalancerTask.java  |  47 +---
 .../TestContainerBalancerDatanodeNodeLimit.java    |  88 ++++++
 6 files changed, 922 insertions(+), 41 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerClusterAnalyzer.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerClusterAnalyzer.java
new file mode 100644
index 00000000000..56a5675b18d
--- /dev/null
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerClusterAnalyzer.java
@@ -0,0 +1,272 @@
+/*
+ * 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.Comparator;
+import java.util.List;
+import java.util.Set;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos.DatanodeUsageInfoProto;
+import 
org.apache.hadoop.hdds.scm.container.balancer.ContainerBalancerClusterSnapshot.NodeUtilization;
+
+/**
+ * Classifies datanode usage protos using the same rules as
+ * {@code ContainerBalancerTask.initializeIteration()}.
+ */
+public final class ContainerBalancerClusterAnalyzer {
+
+  private static final int TOP_NODE_LIMIT = 5;
+
+  private ContainerBalancerClusterAnalyzer() {
+  }
+
+  /**
+   * Core shared formula: (totalCapacity - totalRemaining) / totalCapacity.
+   */
+  public static double calculateAvgUtilization(long totalCapacity, long 
totalRemaining) {
+    if (totalCapacity == 0) {
+      return 0;
+    }
+    return (totalCapacity - totalRemaining) / (double) totalCapacity;
+  }
+
+  /**
+   * Cluster average from datanode usage protos.
+   */
+  public static double calculateAvgUtilization(List<DatanodeUsageInfoProto> 
nodes) {
+    if (nodes.isEmpty()) {
+      return 0;
+    }
+    long totalCapacity = 0;
+    long totalRemaining = 0;
+    for (DatanodeUsageInfoProto node : nodes) {
+      totalCapacity += node.getCapacity();
+      totalRemaining += node.getRemaining();
+    }
+    return calculateAvgUtilization(totalCapacity, totalRemaining);
+  }
+
+  /**
+   * Returns true if utilization is above the upper limit (source candidate).
+   */
+  public static boolean isOverUtilized(double utilization, double upperLimit) {
+    return Double.compare(utilization, upperLimit) > 0;
+  }
+
+  /**
+   * Returns true if utilization is below the lower limit (target candidate).
+   */
+  public static boolean isUnderUtilized(double utilization, double lowerLimit) 
{
+    return Double.compare(utilization, lowerLimit) < 0;
+  }
+
+  /**
+   * Bytes above the upper limit on an over-utilized node.
+   */
+  public static long overUtilizedBytes(long capacity, double utilization, 
double upperLimit) {
+    return ratioToBytes(capacity, utilization) - ratioToBytes(capacity, 
upperLimit);
+  }
+
+  /**
+   * Bytes below the lower limit on an under-utilized node.
+   */
+  public static long underUtilizedBytes(long capacity, double utilization, 
double lowerLimit) {
+    return ratioToBytes(capacity, lowerLimit) - ratioToBytes(capacity, 
utilization);
+  }
+
+  /**
+   * Upper utilization band: cluster average plus threshold.
+   * Nodes above this are candidates to be source nodes.
+   */
+  public static double computeUpperLimit(double clusterAvgUtilization, double 
thresholdRatio) {
+    return clusterAvgUtilization + thresholdRatio;
+  }
+
+  /**
+   * Lower utilization band: cluster average minus threshold.
+   * Nodes below this are candidates to be target nodes.
+   */
+  public static double computeLowerLimit(double clusterAvgUtilization, double 
thresholdRatio) {
+    return clusterAvgUtilization - thresholdRatio;
+  }
+
+  /**
+   * Builds a cluster snapshot after applying include/exclude filters.
+   *
+   * @param nodes all nodes from getDatanodeUsageInfo (typically healthy 
IN_SERVICE)
+   * @param thresholdRatio threshold as ratio, e.g. 0.10 for 10%
+   * @param includeNodes empty = all included; non-empty = allow-list
+   * @param excludeNodes nodes to skip
+   */
+  public static ContainerBalancerClusterSnapshot analyze(
+      List<DatanodeUsageInfoProto> nodes,
+      double thresholdRatio,
+      Set<String> includeNodes,
+      Set<String> excludeNodes) {
+    List<DatanodeUsageInfoProto> eligible = filterEligibleNodes(nodes, 
includeNodes, excludeNodes);
+
+    if (eligible.isEmpty()) {
+      return emptySnapshot(thresholdRatio);
+    }
+
+    double clusterAvgUtilization = calculateAvgUtilization(eligible);
+    double upperLimit = computeUpperLimit(clusterAvgUtilization, 
thresholdRatio);
+    double lowerLimit = computeLowerLimit(clusterAvgUtilization, 
thresholdRatio);
+
+    List<NodeUtilization> sources = new ArrayList<>();
+    List<NodeUtilization> targets = new ArrayList<>();
+    double maxUtilization = Double.NEGATIVE_INFINITY;
+    double minUtilization = Double.POSITIVE_INFINITY;
+    long clusterCapacityBytes = 0;
+    long totalOverUtilizedBytes = 0;
+    long totalUnderUtilizedBytes = 0;
+
+    for (DatanodeUsageInfoProto node : eligible) {
+      long capacity = node.getCapacity();
+      clusterCapacityBytes += capacity;
+      double utilization = calculateNodeUtilization(node);
+
+      maxUtilization = Math.max(maxUtilization, utilization);
+      minUtilization = Math.min(minUtilization, utilization);
+
+      String hostname = getDisplayHostname(node);
+      if (isOverUtilized(utilization, upperLimit)) {
+        totalOverUtilizedBytes += overUtilizedBytes(capacity, utilization, 
upperLimit);
+        sources.add(new NodeUtilization(hostname, utilization));
+      } else if (isUnderUtilized(utilization, lowerLimit)) {
+        totalUnderUtilizedBytes += underUtilizedBytes(capacity, utilization, 
lowerLimit);
+        targets.add(new NodeUtilization(hostname, utilization));
+      }
+    }
+
+    
sources.sort(Comparator.comparingDouble(NodeUtilization::getUtilization).reversed());
+    targets.sort(Comparator.comparingDouble(NodeUtilization::getUtilization));
+
+    double imbalance = maxUtilization - minUtilization;
+
+    return ContainerBalancerClusterSnapshot.newBuilder()
+        .setTotalEligibleDatanodes(eligible.size())
+        .setClusterAvgUtilization(clusterAvgUtilization)
+        .setClusterCapacityBytes(clusterCapacityBytes)
+        .setMaxUtilization(maxUtilization)
+        .setMinUtilization(minUtilization)
+        .setUpperLimit(upperLimit)
+        .setLowerLimit(lowerLimit)
+        .setSourceCount(sources.size())
+        .setTargetCount(targets.size())
+        .setTotalOverUtilizedBytes(totalOverUtilizedBytes)
+        .setTotalUnderUtilizedBytes(totalUnderUtilizedBytes)
+        .setImbalance(imbalance)
+        .setTopSourceNodes(topNodes(sources))
+        .setBottomTargetNodes(topNodes(targets))
+        .build();
+  }
+
+  /**
+   * Function that returns whether a datanode should be excluded from cluster 
analysis.
+   * A datanode is excluded if it appears in excludeNodes set, or if
+   * includeNodes is non-empty and the datanode is not listed there
+   * by hostname or IP address.
+   */
+  public static boolean shouldExcludeDatanode(DatanodeDetails datanode,
+                                              Set<String> excludeNodes, 
Set<String> includeNodes) {
+    if (excludeNodes.contains(datanode.getHostName()) ||
+        excludeNodes.contains(datanode.getIpAddress())) {
+      return true;
+    } else if (!includeNodes.isEmpty()) {
+      return !includeNodes.contains(datanode.getHostName()) &&
+          !includeNodes.contains(datanode.getIpAddress());
+    }
+    return false;
+  }
+
+  static double calculateNodeUtilization(DatanodeUsageInfoProto node) {
+    long capacity = node.getCapacity();
+    if (capacity == 0) {
+      return 0;
+    }
+    return (capacity - node.getRemaining()) / (double) capacity;
+  }
+
+  private static List<DatanodeUsageInfoProto> filterEligibleNodes(
+      List<DatanodeUsageInfoProto> nodes,
+      Set<String> includeNodes,
+      Set<String> excludeNodes) {
+    List<DatanodeUsageInfoProto> eligible = new ArrayList<>();
+    for (DatanodeUsageInfoProto node : nodes) {
+      if (!node.hasNode()) {
+        continue;
+      }
+      DatanodeDetails datanode = 
DatanodeDetails.getFromProtoBuf(node.getNode());
+      if (!shouldExcludeDatanode(datanode, excludeNodes, includeNodes)) {
+        eligible.add(node);
+      }
+    }
+    return eligible;
+  }
+
+  /**
+   * Calculates the number of used bytes given capacity and utilization ratio.
+   *
+   * @param nodeCapacity     capacity of the node.
+   * @param utilizationRatio used space by capacity ratio of the node.
+   * @return number of bytes
+   */
+  public static long ratioToBytes(long nodeCapacity, double utilizationRatio) {
+    return (long) (nodeCapacity * utilizationRatio);
+  }
+
+  private static String getDisplayHostname(DatanodeUsageInfoProto node) {
+    DatanodeDetails datanode = DatanodeDetails.getFromProtoBuf(node.getNode());
+    String hostname = datanode.getHostName();
+    if (hostname != null && !hostname.isEmpty()) {
+      return hostname;
+    }
+    return datanode.getIpAddress();
+  }
+
+  private static List<NodeUtilization> topNodes(List<NodeUtilization> nodes) {
+    int limit = Math.min(TOP_NODE_LIMIT, nodes.size());
+    List<NodeUtilization> result = new ArrayList<>(limit);
+    for (int i = 0; i < limit; i++) {
+      result.add(nodes.get(i));
+    }
+    return result;
+  }
+
+  private static ContainerBalancerClusterSnapshot emptySnapshot(double 
thresholdRatio) {
+    return ContainerBalancerClusterSnapshot.newBuilder()
+        .setTotalEligibleDatanodes(0)
+        .setClusterAvgUtilization(0)
+        .setClusterCapacityBytes(0)
+        .setMaxUtilization(0)
+        .setMinUtilization(0)
+        .setUpperLimit(computeUpperLimit(0, thresholdRatio))
+        .setLowerLimit(computeLowerLimit(0, thresholdRatio))
+        .setSourceCount(0)
+        .setTargetCount(0)
+        .setTotalOverUtilizedBytes(0)
+        .setTotalUnderUtilizedBytes(0)
+        .setImbalance(0)
+        .setTopSourceNodes(Collections.emptyList())
+        .setBottomTargetNodes(Collections.emptyList())
+        .build();
+  }
+}
diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerClusterSnapshot.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerClusterSnapshot.java
new file mode 100644
index 00000000000..7c74c16763e
--- /dev/null
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerClusterSnapshot.java
@@ -0,0 +1,250 @@
+/*
+ * 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.List;
+import java.util.Objects;
+
+/**
+ * Point-in-time cluster imbalance view built from datanode usage protos.
+ */
+public final class ContainerBalancerClusterSnapshot {
+
+  private final int totalEligibleDatanodes;
+  private final double clusterAvgUtilization;
+  private final long clusterCapacityBytes;
+  private final double maxUtilization;
+  private final double minUtilization;
+  private final double upperLimit;
+  private final double lowerLimit;
+  private final int sourceCount;
+  private final int targetCount;
+  private final long totalOverUtilizedBytes;
+  private final long totalUnderUtilizedBytes;
+
+  // bytesToMove has same value as totalOverUtilizedBytes. The analyzer sets it
+  // once via setTotalOverUtilizedBytes(), bytesToMove is assigned here for 
assessment output.
+  private final long bytesToMove;
+  private final double imbalance;
+  private final List<NodeUtilization> topSourceNodes;
+  private final List<NodeUtilization> bottomTargetNodes;
+
+  private ContainerBalancerClusterSnapshot(Builder b) {
+    this.totalEligibleDatanodes = b.totalEligibleDatanodes;
+    this.clusterAvgUtilization = b.clusterAvgUtilization;
+    this.clusterCapacityBytes = b.clusterCapacityBytes;
+    this.maxUtilization = b.maxUtilization;
+    this.minUtilization = b.minUtilization;
+    this.upperLimit = b.upperLimit;
+    this.lowerLimit = b.lowerLimit;
+    this.sourceCount = b.sourceCount;
+    this.targetCount = b.targetCount;
+    this.totalOverUtilizedBytes = b.totalOverUtilizedBytes;
+    this.totalUnderUtilizedBytes = b.totalUnderUtilizedBytes;
+    this.bytesToMove = b.totalOverUtilizedBytes;
+    this.imbalance = b.imbalance;
+    this.topSourceNodes = Collections.unmodifiableList(
+        Objects.requireNonNull(b.topSourceNodes));
+    this.bottomTargetNodes = Collections.unmodifiableList(
+        Objects.requireNonNull(b.bottomTargetNodes));
+  }
+
+  public int getTotalEligibleDatanodes() {
+    return totalEligibleDatanodes;
+  }
+
+  public double getClusterAvgUtilization() {
+    return clusterAvgUtilization;
+  }
+
+  public long getClusterCapacityBytes() {
+    return clusterCapacityBytes;
+  }
+
+  public double getMaxUtilization() {
+    return maxUtilization;
+  }
+
+  public double getMinUtilization() {
+    return minUtilization;
+  }
+
+  public double getUpperLimit() {
+    return upperLimit;
+  }
+
+  public double getLowerLimit() {
+    return lowerLimit;
+  }
+
+  public int getSourceCount() {
+    return sourceCount;
+  }
+
+  public int getTargetCount() {
+    return targetCount;
+  }
+
+  public long getTotalOverUtilizedBytes() {
+    return totalOverUtilizedBytes;
+  }
+
+  public long getTotalUnderUtilizedBytes() {
+    return totalUnderUtilizedBytes;
+  }
+
+  public long getBytesToMove() {
+    return bytesToMove;
+  }
+
+  public double getImbalance() {
+    return imbalance;
+  }
+
+  public List<NodeUtilization> getTopSourceNodes() {
+    return topSourceNodes;
+  }
+
+  public List<NodeUtilization> getBottomTargetNodes() {
+    return bottomTargetNodes;
+  }
+
+  public static Builder newBuilder() {
+    return new Builder();
+  }
+
+  /**
+   * Builder class for building ContainerBalancerClusterSnapshot.
+   */
+  public static final class Builder {
+    private int totalEligibleDatanodes;
+    private double clusterAvgUtilization;
+    private long clusterCapacityBytes;
+    private double maxUtilization;
+    private double minUtilization;
+    private double upperLimit;
+    private double lowerLimit;
+    private int sourceCount;
+    private int targetCount;
+    private long totalOverUtilizedBytes;
+    private long totalUnderUtilizedBytes;
+    private double imbalance;
+    private List<NodeUtilization> topSourceNodes = Collections.emptyList();
+    private List<NodeUtilization> bottomTargetNodes = Collections.emptyList();
+
+    private Builder() {
+    }
+
+    public Builder setTotalEligibleDatanodes(int totalEligibleDatanodes) {
+      this.totalEligibleDatanodes = totalEligibleDatanodes;
+      return this;
+    }
+
+    public Builder setClusterAvgUtilization(double clusterAvgUtilization) {
+      this.clusterAvgUtilization = clusterAvgUtilization;
+      return this;
+    }
+
+    public Builder setClusterCapacityBytes(long clusterCapacityBytes) {
+      this.clusterCapacityBytes = clusterCapacityBytes;
+      return this;
+    }
+
+    public Builder setMaxUtilization(double maxUtilization) {
+      this.maxUtilization = maxUtilization;
+      return this;
+    }
+
+    public Builder setMinUtilization(double minUtilization) {
+      this.minUtilization = minUtilization;
+      return this;
+    }
+
+    public Builder setUpperLimit(double upperLimit) {
+      this.upperLimit = upperLimit;
+      return this;
+    }
+
+    public Builder setLowerLimit(double lowerLimit) {
+      this.lowerLimit = lowerLimit;
+      return this;
+    }
+
+    public Builder setSourceCount(int sourceCount) {
+      this.sourceCount = sourceCount;
+      return this;
+    }
+
+    public Builder setTargetCount(int targetCount) {
+      this.targetCount = targetCount;
+      return this;
+    }
+
+    public Builder setTotalOverUtilizedBytes(long totalOverUtilizedBytes) {
+      this.totalOverUtilizedBytes = totalOverUtilizedBytes;
+      return this;
+    }
+
+    public Builder setTotalUnderUtilizedBytes(long totalUnderUtilizedBytes) {
+      this.totalUnderUtilizedBytes = totalUnderUtilizedBytes;
+      return this;
+    }
+
+    public Builder setImbalance(double imbalance) {
+      this.imbalance = imbalance;
+      return this;
+    }
+
+    public Builder setTopSourceNodes(List<NodeUtilization> topSourceNodes) {
+      this.topSourceNodes = topSourceNodes;
+      return this;
+    }
+
+    public Builder setBottomTargetNodes(List<NodeUtilization> 
bottomTargetNodes) {
+      this.bottomTargetNodes = bottomTargetNodes;
+      return this;
+    }
+
+    public ContainerBalancerClusterSnapshot build() {
+      return new ContainerBalancerClusterSnapshot(this);
+    }
+  }
+
+  /**
+   * Hostname and utilization ratio of a single datanode, retained on the 
snapshot
+   * for per-node display in CLI output (e.g., assessment).
+   */
+  public static final class NodeUtilization {
+    private final String hostname;
+    private final double utilization;
+
+    public NodeUtilization(String hostname, double utilization) {
+      this.hostname = hostname;
+      this.utilization = utilization;
+    }
+
+    public String getHostname() {
+      return hostname;
+    }
+
+    public double getUtilization() {
+      return utilization;
+    }
+  }
+}
diff --git 
a/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancerClusterAnalyzer.java
 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancerClusterAnalyzer.java
new file mode 100644
index 00000000000..004d78feecc
--- /dev/null
+++ 
b/hadoop-hdds/common/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancerClusterAnalyzer.java
@@ -0,0 +1,296 @@
+/*
+ * 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.apache.hadoop.ozone.ClientVersion.DEFAULT_VERSION;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos.DatanodeUsageInfoProto;
+import 
org.apache.hadoop.hdds.scm.container.balancer.ContainerBalancerClusterSnapshot.NodeUtilization;
+import org.junit.jupiter.api.Test;
+
+/** Tests for {@link ContainerBalancerClusterAnalyzer}. */
+public final class TestContainerBalancerClusterAnalyzer {
+
+  private static final double THRESHOLD = 0.10;
+
+  @Test
+  void testCalculateAvgUtilizationFromTotals() {
+    assertEquals(0.70d, ContainerBalancerClusterAnalyzer
+        .calculateAvgUtilization(200, 60), 0.0001);
+    assertEquals(0, ContainerBalancerClusterAnalyzer
+        .calculateAvgUtilization(0, 0));
+  }
+
+  @Test
+  void testBalancedClusterHasNoSourcesOrTargets() {
+    List<DatanodeUsageInfoProto> nodes = Arrays.asList(
+        proto("dn-1", 100, 30),
+        proto("dn-2", 100, 30));
+
+    ContainerBalancerClusterSnapshot snapshot =
+        ContainerBalancerClusterAnalyzer.analyze(nodes, THRESHOLD,
+            Collections.emptySet(), Collections.emptySet());
+
+    assertEquals(2, snapshot.getTotalEligibleDatanodes());
+    assertEquals(0.70d, snapshot.getClusterAvgUtilization(), 0.0001);
+    assertEquals(0, snapshot.getSourceCount());
+    assertEquals(0, snapshot.getTargetCount());
+    assertEquals(0, snapshot.getBytesToMove());
+    assertTrue(snapshot.getTopSourceNodes().isEmpty());
+    assertTrue(snapshot.getBottomTargetNodes().isEmpty());
+  }
+
+  @Test
+  void testOverAndUnderUtilizedClassification() {
+    List<DatanodeUsageInfoProto> nodes = Arrays.asList(
+        proto("source-1", 100, 10),
+        proto("target-1", 100, 50));
+
+    ContainerBalancerClusterSnapshot snapshot =
+        ContainerBalancerClusterAnalyzer.analyze(nodes, THRESHOLD,
+            Collections.emptySet(), Collections.emptySet());
+
+    assertEquals(1, snapshot.getSourceCount());
+    assertEquals(1, snapshot.getTargetCount());
+    assertEquals(0.90d, snapshot.getMaxUtilization(), 0.0001);
+    assertEquals(0.50d, snapshot.getMinUtilization(), 0.0001);
+    assertEquals(0.40d, snapshot.getImbalance(), 0.0001);
+    assertEquals(10, snapshot.getTotalOverUtilizedBytes());
+    assertEquals(10, snapshot.getTotalUnderUtilizedBytes());
+    assertEquals(snapshot.getTotalOverUtilizedBytes(), 
snapshot.getBytesToMove());
+    assertEquals(Collections.singletonList("source-1"),
+        snapshot.getTopSourceNodes().stream()
+            .map(NodeUtilization::getHostname).collect(Collectors.toList()));
+    assertEquals(Collections.singletonList("target-1"),
+        snapshot.getBottomTargetNodes().stream()
+            .map(NodeUtilization::getHostname).collect(Collectors.toList()));
+  }
+
+  @Test
+  void testIncludeExcludeFilters() {
+    List<DatanodeUsageInfoProto> nodes = Arrays.asList(
+        proto("keep-me", 100, 10),    // 90%
+        proto("also-keep", 100, 50),  // 50%
+        proto("drop-me", 100, 30));   // filtered out
+
+    Set<String> include = new HashSet<>(Arrays.asList("keep-me", "also-keep"));
+
+    ContainerBalancerClusterSnapshot snapshot =
+        ContainerBalancerClusterAnalyzer.analyze(nodes, THRESHOLD, include,
+            Collections.emptySet());
+
+    assertEquals(2, snapshot.getTotalEligibleDatanodes());
+    assertEquals(1, snapshot.getSourceCount());  // keep-me
+    assertEquals(1, snapshot.getTargetCount()); // also-keep
+  }
+
+  @Test
+  void testEmptyAfterFilterReturnsEmptySnapshot() {
+    List<DatanodeUsageInfoProto> nodes = Collections.singletonList(
+        proto("excluded", 100, 10));
+
+    ContainerBalancerClusterSnapshot snapshot =
+        ContainerBalancerClusterAnalyzer.analyze(nodes, THRESHOLD,
+            Collections.emptySet(), Collections.singleton("excluded"));
+
+    assertEquals(0, snapshot.getTotalEligibleDatanodes());
+    assertEquals(0, snapshot.getClusterCapacityBytes());
+    assertEquals(0, snapshot.getBytesToMove());
+  }
+
+  @Test
+  void testZeroCapacityNodeTreatedAsZeroUtilization() {
+    List<DatanodeUsageInfoProto> nodes = Arrays.asList(
+        proto("zero-cap", 0, 0),
+        proto("normal", 100, 30));
+
+    ContainerBalancerClusterSnapshot snapshot =
+        ContainerBalancerClusterAnalyzer.analyze(nodes, THRESHOLD,
+            Collections.emptySet(), Collections.emptySet());
+
+    assertEquals(2, snapshot.getTotalEligibleDatanodes());
+    assertEquals(0, snapshot.getMinUtilization(), 0.0001);
+  }
+
+  @Test
+  void testProtoWithoutNodeIsSkipped() {
+    DatanodeUsageInfoProto noNode = DatanodeUsageInfoProto.newBuilder()
+        .setCapacity(100)
+        .setRemaining(10)
+        .setUsed(90)
+        .build();
+
+    ContainerBalancerClusterSnapshot snapshot =
+        ContainerBalancerClusterAnalyzer.analyze(
+            Collections.singletonList(noNode), THRESHOLD,
+            Collections.emptySet(), Collections.emptySet());
+
+    assertEquals(0, snapshot.getTotalEligibleDatanodes());
+  }
+
+  @Test
+  void testEmptyInputListReturnsEmptySnapshot() {
+    ContainerBalancerClusterSnapshot snapshot =
+        ContainerBalancerClusterAnalyzer.analyze(
+            Collections.emptyList(), THRESHOLD,
+            Collections.emptySet(), Collections.emptySet());
+
+    assertEquals(0, snapshot.getTotalEligibleDatanodes());
+    assertEquals(0, snapshot.getClusterAvgUtilization(), 0.0001);
+    assertEquals(0, snapshot.getBytesToMove());
+  }
+
+  @Test
+  void testExcludeByIpAddress() {
+    DatanodeDetails datanode = DatanodeDetails.newBuilder()
+        .setHostName("dn-host")
+        .setIpAddress("10.0.0.5")
+        .setUuid(UUID.randomUUID())
+        .build();
+    DatanodeUsageInfoProto node = DatanodeUsageInfoProto.newBuilder()
+        .setNode(datanode.toProto(DEFAULT_VERSION.toProtoValue()))
+        .setCapacity(100)
+        .setRemaining(10)
+        .setUsed(90)
+        .build();
+
+    ContainerBalancerClusterSnapshot snapshot =
+        ContainerBalancerClusterAnalyzer.analyze(
+            Collections.singletonList(node), THRESHOLD,
+            Collections.emptySet(), Collections.singleton("10.0.0.5"));
+
+    assertEquals(0, snapshot.getTotalEligibleDatanodes());
+  }
+
+  @Test
+  void testExcludeWinsOverInclude() {
+    List<DatanodeUsageInfoProto> nodes = Collections.singletonList(
+        proto("both-lists", 100, 10));
+
+    Set<String> include = Collections.singleton("both-lists");
+    Set<String> exclude = Collections.singleton("both-lists");
+
+    ContainerBalancerClusterSnapshot snapshot =
+        ContainerBalancerClusterAnalyzer.analyze(nodes, THRESHOLD, include, 
exclude);
+
+    assertEquals(0, snapshot.getTotalEligibleDatanodes());
+  }
+
+  @Test
+  void testTopFiveSourcesAndBottomFiveTargets() {
+    List<DatanodeUsageInfoProto> nodes = new ArrayList<>();
+    for (int i = 0; i < 7; i++) {
+      nodes.add(proto(String.format("source-%02d", i), 100, 10));
+    }
+    for (int i = 0; i < 7; i++) {
+      nodes.add(proto(String.format("target-%02d", i), 100, 50));
+    }
+
+    ContainerBalancerClusterSnapshot snapshot =
+        ContainerBalancerClusterAnalyzer.analyze(nodes, THRESHOLD,
+            Collections.emptySet(), Collections.emptySet());
+
+    assertEquals(7, snapshot.getSourceCount());
+    assertEquals(7, snapshot.getTargetCount());
+    assertEquals(5, snapshot.getTopSourceNodes().size());
+    assertEquals(5, snapshot.getBottomTargetNodes().size());
+    Set<String> expectedSources = new HashSet<>();
+    for (int i = 0; i < 7; i++) {
+      expectedSources.add(String.format("source-%02d", i));
+    }
+    Set<String> expectedTargets = new HashSet<>();
+    for (int i = 0; i < 7; i++) {
+      expectedTargets.add(String.format("target-%02d", i));
+    }
+    assertTrue(expectedSources.containsAll(
+        snapshot.getTopSourceNodes().stream()
+            .map(NodeUtilization::getHostname).collect(Collectors.toList())));
+    assertTrue(expectedTargets.containsAll(
+        snapshot.getBottomTargetNodes().stream()
+            .map(NodeUtilization::getHostname).collect(Collectors.toList())));
+  }
+
+  @Test
+  void testTopSourceNodesCarryUtilization() {
+    List<DatanodeUsageInfoProto> nodes = Arrays.asList(
+        proto("source-hot", 100, 5),   // 95% used
+        proto("source-warm", 100, 15), // 85% used
+        proto("balanced", 100, 50),
+        proto("target-cool", 100, 90)  // 10% used
+    );
+
+    ContainerBalancerClusterSnapshot snapshot =
+        ContainerBalancerClusterAnalyzer.analyze(nodes, THRESHOLD,
+            Collections.emptySet(), Collections.emptySet());
+
+    List<NodeUtilization> top = snapshot.getTopSourceNodes();
+    assertEquals("source-hot", top.get(0).getHostname());
+    assertEquals(0.95, top.get(0).getUtilization(), 1e-9);
+    assertEquals("source-warm", top.get(1).getHostname());
+    assertEquals(0.85, top.get(1).getUtilization(), 1e-9);
+
+    List<NodeUtilization> bottom = snapshot.getBottomTargetNodes();
+    assertEquals("target-cool", bottom.get(0).getHostname());
+    assertEquals(0.10, bottom.get(0).getUtilization(), 1e-9);
+  }
+
+  @Test
+  void testShouldExcludeDatanodeDirectly() {
+    DatanodeDetails datanode = DatanodeDetails.newBuilder()
+        .setHostName("dn-1")
+        .setIpAddress("10.0.0.1")
+        .setUuid(UUID.randomUUID())
+        .build();
+
+    assertTrue(ContainerBalancerClusterAnalyzer.shouldExcludeDatanode(
+        datanode, Collections.singleton("dn-1"), Collections.emptySet()));
+    assertTrue(ContainerBalancerClusterAnalyzer.shouldExcludeDatanode(
+        datanode, Collections.singleton("10.0.0.1"), Collections.emptySet()));
+    assertTrue(ContainerBalancerClusterAnalyzer.shouldExcludeDatanode(
+        datanode, Collections.emptySet(), Collections.singleton("other")));
+    assertFalse(ContainerBalancerClusterAnalyzer.shouldExcludeDatanode(
+        datanode, Collections.emptySet(), Collections.singleton("dn-1")));
+  }
+
+  private static DatanodeUsageInfoProto proto(String hostname, long capacity, 
long remaining) {
+    DatanodeDetails datanode = DatanodeDetails.newBuilder()
+        .setHostName(hostname)
+        .setIpAddress("127.0.0.1")
+        .setUuid(UUID.randomUUID())
+        .build();
+    long used = capacity - remaining;
+    return DatanodeUsageInfoProto.newBuilder()
+        .setNode(datanode.toProto(DEFAULT_VERSION.toProtoValue()))
+        .setCapacity(capacity)
+        .setRemaining(remaining)
+        .setUsed(used)
+        .build();
+  }
+}
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancer.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancer.java
index 159551fe4ca..14aa7efdc38 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancer.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancer.java
@@ -660,14 +660,8 @@ private int 
countEligibleDatanodes(ContainerBalancerConfiguration conf) {
 
   static boolean shouldExcludeDatanode(DatanodeDetails datanode,
       Set<String> excludeNodes, Set<String> includeNodes) {
-    if (excludeNodes.contains(datanode.getHostName()) ||
-        excludeNodes.contains(datanode.getIpAddress())) {
-      return true;
-    } else if (!includeNodes.isEmpty()) {
-      return !includeNodes.contains(datanode.getHostName()) &&
-          !includeNodes.contains(datanode.getIpAddress());
-    }
-    return false;
+    return ContainerBalancerClusterAnalyzer.shouldExcludeDatanode(
+        datanode, excludeNodes, includeNodes);
   }
 
   public ContainerBalancerMetrics getMetrics() {
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 358d696d5b8..4dff0265445 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
@@ -563,9 +563,11 @@ private boolean initializeIteration() {
 
     double threshold = config.getThresholdAsRatio();
     // over utilized nodes have utilization(that is, used / capacity) greater 
than upper limit
-    this.upperLimit = clusterAvgUtilisation + threshold;
+    this.upperLimit = ContainerBalancerClusterAnalyzer.computeUpperLimit(
+        clusterAvgUtilisation, threshold);
     // under utilized nodes have utilization(that is, used / capacity) less 
than lower limit
-    this.lowerLimit = clusterAvgUtilisation - threshold;
+    this.lowerLimit = ContainerBalancerClusterAnalyzer.computeLowerLimit(
+        clusterAvgUtilisation, threshold);
 
     if (LOG.isDebugEnabled()) {
       LOG.debug("Lower limit for utilization is {} and Upper limit for 
utilization is {}", lowerLimit, upperLimit);
@@ -587,28 +589,18 @@ private boolean initializeIteration() {
             datanodeUsageInfo.getScmNodeStat().getRemaining().get(),
             utilization);
       }
-      if (Double.compare(utilization, upperLimit) > 0) {
+      if (ContainerBalancerClusterAnalyzer.isOverUtilized(utilization, 
upperLimit)) {
         overUtilizedNodes.add(datanodeUsageInfo);
         metrics.incrementNumDatanodesUnbalanced(1);
-
-        // amount of bytes greater than upper limit in this node
-        long overUtilizedBytes = ratioToBytes(
-            datanodeUsageInfo.getScmNodeStat().getCapacity().get(),
-            utilization) - ratioToBytes(
-            datanodeUsageInfo.getScmNodeStat().getCapacity().get(),
-            upperLimit);
-        totalOverUtilizedBytes += overUtilizedBytes;
-      } else if (Double.compare(utilization, lowerLimit) < 0) {
+        totalOverUtilizedBytes +=
+            
ContainerBalancerClusterAnalyzer.overUtilizedBytes(datanodeUsageInfo.getScmNodeStat().getCapacity().get(),
+                utilization, upperLimit);
+      } else if (ContainerBalancerClusterAnalyzer.isUnderUtilized(utilization, 
lowerLimit)) {
         underUtilizedNodes.add(datanodeUsageInfo);
         metrics.incrementNumDatanodesUnbalanced(1);
-
-        // amount of bytes lesser than lower limit in this node
-        long underUtilizedBytes = ratioToBytes(
-            datanodeUsageInfo.getScmNodeStat().getCapacity().get(),
-            lowerLimit) - ratioToBytes(
-            datanodeUsageInfo.getScmNodeStat().getCapacity().get(),
-            utilization);
-        totalUnderUtilizedBytes += underUtilizedBytes;
+        totalUnderUtilizedBytes +=
+            
ContainerBalancerClusterAnalyzer.underUtilizedBytes(datanodeUsageInfo.getScmNodeStat().getCapacity().get(),
+                utilization, lowerLimit);
       }
     }
     metrics.incrementDataSizeUnbalancedGB(
@@ -1128,17 +1120,6 @@ private void updateTargetsAndSelectionCriteria(
     selectedSources.add(source);
   }
 
-  /**
-   * Calculates the number of used bytes given capacity and utilization ratio.
-   *
-   * @param nodeCapacity     capacity of the node.
-   * @param utilizationRatio used space by capacity ratio of the node.
-   * @return number of bytes
-   */
-  private long ratioToBytes(Long nodeCapacity, double utilizationRatio) {
-    return (long) (nodeCapacity * utilizationRatio);
-  }
-
   /**
    * Calculates the average utilization for the specified nodes.
    * Utilization is (capacity - remaining) divided by capacity.
@@ -1160,8 +1141,8 @@ public static double 
calculateAvgUtilization(List<DatanodeUsageInfo> nodes) {
     }
     long clusterCapacity = aggregatedStats.getCapacity().get();
     long clusterRemaining = aggregatedStats.getRemaining().get();
-
-    return (clusterCapacity - clusterRemaining) / (double) clusterCapacity;
+    return ContainerBalancerClusterAnalyzer.calculateAvgUtilization(
+        clusterCapacity, clusterRemaining);
   }
 
   /**
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancerDatanodeNodeLimit.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancerDatanodeNodeLimit.java
index f37eaab9654..1b4f420cf1a 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancerDatanodeNodeLimit.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancerDatanodeNodeLimit.java
@@ -35,6 +35,7 @@
 import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -46,6 +47,7 @@
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
 import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos.DatanodeUsageInfoProto;
 import 
org.apache.hadoop.hdds.protocol.proto.StorageContainerDatanodeProtocolProtos;
 import org.apache.hadoop.hdds.scm.ContainerPlacementStatus;
 import org.apache.hadoop.hdds.scm.container.ContainerID;
@@ -53,9 +55,11 @@
 import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException;
 import org.apache.hadoop.hdds.scm.container.ContainerReplica;
 import org.apache.hadoop.hdds.scm.container.ContainerReplicaNotFoundException;
+import 
org.apache.hadoop.hdds.scm.container.balancer.ContainerBalancerClusterSnapshot.NodeUtilization;
 import org.apache.hadoop.hdds.scm.node.DatanodeUsageInfo;
 import org.apache.hadoop.hdds.scm.node.NodeStatus;
 import org.apache.hadoop.hdds.scm.node.states.NodeNotFoundException;
+import org.apache.hadoop.ozone.ClientVersion;
 import org.apache.hadoop.ozone.OzoneConsts;
 import org.apache.ozone.test.GenericTestUtils;
 import org.junit.jupiter.api.BeforeAll;
@@ -96,6 +100,90 @@ private static Stream<Arguments> createMockedSCMs() {
         Arguments.of(getMockedSCM(30)));
   }
 
+  @ParameterizedTest(name = "MockedSCM #{index}: {0}")
+  @MethodSource("createMockedSCMs")
+  void analyzerMatchesTaskAfterInitializeIteration(@Nonnull MockedSCM 
mockedSCM) {
+    ContainerBalancerConfiguration config =
+        new ContainerBalancerConfigBuilder(mockedSCM.getNodeCount()).build();
+
+    ContainerBalancerTask task = mockedSCM.startBalancerTask(config);
+
+    List<DatanodeUsageInfo> eligible = 
mockedSCM.getNodeManager().getMostOrLeastUsedDatanodes(true)
+        .stream()
+        .filter(n -> !ContainerBalancerClusterAnalyzer.shouldExcludeDatanode(
+            n.getDatanodeDetails(), config.getExcludeNodes(), 
config.getIncludeNodes()))
+        .collect(Collectors.toList());
+    List<DatanodeUsageInfoProto> protos = eligible.stream()
+        .map(n -> n.toProto(ClientVersion.DEFAULT_VERSION.toProtoValue()))
+        .collect(Collectors.toList());
+
+    ContainerBalancerClusterSnapshot snapshot = 
ContainerBalancerClusterAnalyzer.analyze(
+        protos, config.getThresholdAsRatio(), config.getIncludeNodes(), 
config.getExcludeNodes());
+
+    double taskAvg = ContainerBalancerTask.calculateAvgUtilization(eligible);
+    double upperLimit = ContainerBalancerClusterAnalyzer.computeUpperLimit(
+        taskAvg, config.getThresholdAsRatio());
+    double lowerLimit = ContainerBalancerClusterAnalyzer.computeLowerLimit(
+        taskAvg, config.getThresholdAsRatio());
+
+    assertEquals(eligible.size(), snapshot.getTotalEligibleDatanodes());
+    assertEquals(task.getOverUtilizedNodes().size(), 
snapshot.getSourceCount());
+    assertEquals(task.getUnderUtilizedNodes().size(), 
snapshot.getTargetCount());
+    assertEquals(taskAvg, snapshot.getClusterAvgUtilization(), 0.0001);
+    assertEquals(upperLimit, snapshot.getUpperLimit(), 0.0001);
+    assertEquals(lowerLimit, snapshot.getLowerLimit(), 0.0001);
+
+    long expectedOverBytes = task.getOverUtilizedNodes().stream()
+        .mapToLong(n -> {
+          long capacity = n.getScmNodeStat().getCapacity().get();
+          double utilization = n.calculateUtilization();
+          return ContainerBalancerClusterAnalyzer.overUtilizedBytes(
+              capacity, utilization, upperLimit);
+        })
+        .sum();
+    long expectedUnderBytes = task.getUnderUtilizedNodes().stream()
+        .mapToLong(n -> {
+          long capacity = n.getScmNodeStat().getCapacity().get();
+          double utilization = n.calculateUtilization();
+          return ContainerBalancerClusterAnalyzer.underUtilizedBytes(
+              capacity, utilization, lowerLimit);
+        })
+        .sum();
+    assertEquals(expectedOverBytes, snapshot.getTotalOverUtilizedBytes());
+    assertEquals(expectedUnderBytes, snapshot.getTotalUnderUtilizedBytes());
+    assertEquals(expectedOverBytes, snapshot.getBytesToMove());
+    assertEquals(
+        Math.max(snapshot.getTotalOverUtilizedBytes(), 
snapshot.getTotalUnderUtilizedBytes()) / OzoneConsts.GB,
+        task.getMetrics().getDataSizeUnbalancedGB());
+
+    List<NodeUtilization> expectedTopSources = 
task.getOverUtilizedNodes().stream()
+        .sorted(Comparator.comparingDouble((DatanodeUsageInfo n) -> 
n.calculateUtilization()).reversed())
+        .limit(5)
+        .map(n -> new NodeUtilization(n.getDatanodeDetails().getHostName(),
+            n.calculateUtilization()))
+        .collect(Collectors.toList());
+    List<NodeUtilization> expectedBottomTargets = 
task.getUnderUtilizedNodes().stream()
+        .sorted(Comparator.comparingDouble((DatanodeUsageInfo n) -> 
n.calculateUtilization()))
+        .limit(5)
+        .map(n -> new NodeUtilization(n.getDatanodeDetails().getHostName(),
+            n.calculateUtilization()))
+        .collect(Collectors.toList());
+
+    List<NodeUtilization> actualTopSources = snapshot.getTopSourceNodes();
+    assertEquals(expectedTopSources.size(), actualTopSources.size());
+    for (int i = 0; i < expectedTopSources.size(); i++) {
+      assertEquals(expectedTopSources.get(i).getHostname(), 
actualTopSources.get(i).getHostname());
+      assertEquals(expectedTopSources.get(i).getUtilization(), 
actualTopSources.get(i).getUtilization(), 0.0001);
+    }
+
+    List<NodeUtilization> actualBottomTargets = 
snapshot.getBottomTargetNodes();
+    assertEquals(expectedBottomTargets.size(), actualBottomTargets.size());
+    for (int i = 0; i < expectedBottomTargets.size(); i++) {
+      assertEquals(expectedBottomTargets.get(i).getHostname(), 
actualBottomTargets.get(i).getHostname());
+      assertEquals(expectedBottomTargets.get(i).getUtilization(), 
actualBottomTargets.get(i).getUtilization(), 0.0001);
+    }
+  }
+
   @ParameterizedTest(name = "MockedSCM #{index}: {0}")
   @MethodSource("createMockedSCMs")
   public void containerBalancerShouldObeyMaxDatanodesToInvolveLimit(@Nonnull 
MockedSCM mockedSCM) {


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

Reply via email to