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 eccb174c7b3 HDDS-15535. Container Balancer should validate 
configuration and report startup failures to user (#10812)
eccb174c7b3 is described below

commit eccb174c7b353a82a3ff5bbc866eadd7307cfe55
Author: sreejasahithi <[email protected]>
AuthorDate: Wed Jul 29 12:38:19 2026 +0530

    HDDS-15535. Container Balancer should validate configuration and report 
startup failures to user (#10812)
---
 .../balancer/ContainerBalancerConfiguration.java   |  17 ++-
 ...inerLocationProtocolClientSideTranslatorPB.java |   4 +-
 .../scm/container/balancer/ContainerBalancer.java  | 158 +++++++++++++++++++++
 .../container/balancer/ContainerBalancerTask.java  |  16 +--
 .../hdds/scm/server/SCMClientProtocolServer.java   |   4 +-
 .../container/balancer/TestContainerBalancer.java  | 107 +++++++++++++-
 .../scm/cli/ContainerBalancerStartSubcommand.java  |   2 -
 .../datanode/TestContainerBalancerSubCommand.java  |   4 +-
 .../hadoop/hdds/scm/TestFailoverWithSCMHA.java     |   1 +
 .../ozone/TestContainerBalancerOperations.java     |  24 +++-
 10 files changed, 304 insertions(+), 33 deletions(-)

diff --git 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerConfiguration.java
 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerConfiguration.java
index 12ad1501009..65acb9da8e1 100644
--- 
a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerConfiguration.java
+++ 
b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/scm/container/balancer/ContainerBalancerConfiguration.java
@@ -206,7 +206,7 @@ public void setIterations(int count) {
    * Gets the maximum percentage of healthy, in-service datanodes that will be
    * involved in balancing in one iteration.
    *
-   * @return percentage as an integer from 0 up to and including 100
+   * @return percentage as an integer greater than 0 up to and including 100
    */
   public int getMaxDatanodesPercentageToInvolvePerIteration() {
     return maxDatanodesPercentageToInvolvePerIteration;
@@ -253,6 +253,17 @@ public double getMaxDatanodesRatioToInvolvePerIteration() {
     return maxDatanodesPercentageToInvolvePerIteration / 100d;
   }
 
+  /**
+   * Computes the maximum number of datanodes that may be involved in an
+   * iteration for the given eligible datanode count.
+   *
+   * @param eligibleDatanodeCount number of healthy, in-service datanodes.
+   * @return maximum datanodes that may be involved in one iteration
+   */
+  public int computeMaxDatanodesToInvolvePerIteration(int 
eligibleDatanodeCount) {
+    return (int) (getMaxDatanodesRatioToInvolvePerIteration() * 
eligibleDatanodeCount);
+  }
+
   /**
    * Sets the maximum percentage of healthy, in-service datanodes that will be
    * involved in balancing in one iteration.
@@ -266,10 +277,10 @@ public double getMaxDatanodesRatioToInvolvePerIteration() 
{
    */
   public void setMaxDatanodesPercentageToInvolvePerIteration(
       int maxDatanodesPercentageToInvolvePerIteration) {
-    if (maxDatanodesPercentageToInvolvePerIteration < 0 ||
+    if (maxDatanodesPercentageToInvolvePerIteration <= 0 ||
         maxDatanodesPercentageToInvolvePerIteration > 100) {
       throw new IllegalArgumentException(String.format("Argument %d is " +
-              "illegal. Percentage must be from 0 up to and including 100.",
+              "illegal. Percentage must be greater than 0 up to and including 
100.",
           maxDatanodesPercentageToInvolvePerIteration));
     }
     this.maxDatanodesPercentageToInvolvePerIteration =
diff --git 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java
 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java
index 39c47ae3ae0..7808cb286a2 100644
--- 
a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java
+++ 
b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/scm/protocolPB/StorageContainerLocationProtocolClientSideTranslatorPB.java
@@ -1005,9 +1005,9 @@ public StartContainerBalancerResponseProto 
startContainerBalancer(
     }
     if (maxDatanodesPercentageToInvolvePerIteration.isPresent()) {
       int mdti = maxDatanodesPercentageToInvolvePerIteration.get();
-      Preconditions.checkState(mdti >= 0,
+      Preconditions.checkState(mdti > 0,
           "Max Datanodes Percentage To Involve Per Iteration must be " +
-              "greater than equal to zero.");
+              "greater than zero.");
       Preconditions.checkState(mdti <= 100,
           "Max Datanodes Percentage To Involve Per Iteration must be " +
               "lesser than equal to hundred.");
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 3631df49f5a..159551fe4ca 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
@@ -17,10 +17,15 @@
 
 package org.apache.hadoop.hdds.scm.container.balancer;
 
+import static 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeOperationalState.IN_SERVICE;
+import static 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeState.HEALTHY;
+
 import com.google.common.annotations.VisibleForTesting;
 import java.io.IOException;
 import java.time.Duration;
 import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -28,11 +33,16 @@
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.conf.StorageUnit;
 import org.apache.hadoop.hdds.fs.DUFactory;
+import org.apache.hadoop.hdds.protocol.DatanodeDetails;
 import 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.ContainerBalancerConfigurationProto;
 import org.apache.hadoop.hdds.scm.ScmConfigKeys;
+import org.apache.hadoop.hdds.scm.container.ContainerID;
+import org.apache.hadoop.hdds.scm.container.ContainerManager;
+import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException;
 import org.apache.hadoop.hdds.scm.ha.SCMContext;
 import org.apache.hadoop.hdds.scm.ha.StatefulService;
 import org.apache.hadoop.hdds.scm.ha.StatefulServiceDefinition;
+import org.apache.hadoop.hdds.scm.node.DatanodeInfo;
 import org.apache.hadoop.hdds.scm.server.StorageContainerManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -470,6 +480,27 @@ private void 
validateConfiguration(ContainerBalancerConfiguration conf)
               " than ozone.scm.container.size");
     }
 
+    if (conf.getMaxSizeEnteringTarget() > conf.getMaxSizeToMovePerIteration()) 
{
+      LOG.warn("hdds.container.balancer.size.entering.target.max {} should be "
+              + "less than or equal to hdds.container.balancer.size.moved.max"
+              + ".per.iteration {}",
+          conf.getMaxSizeEnteringTarget(), 
conf.getMaxSizeToMovePerIteration());
+      throw new InvalidContainerBalancerConfigurationException(
+          "hdds.container.balancer.size.entering.target.max should be less "
+              + "than or equal to hdds.container.balancer.size.moved.max.per"
+              + ".iteration");
+    }
+    if (conf.getMaxSizeLeavingSource() > conf.getMaxSizeToMovePerIteration()) {
+      LOG.warn("hdds.container.balancer.size.leaving.source.max {} should be "
+              + "less than or equal to hdds.container.balancer.size.moved.max"
+              + ".per.iteration {}",
+          conf.getMaxSizeLeavingSource(), conf.getMaxSizeToMovePerIteration());
+      throw new InvalidContainerBalancerConfigurationException(
+          "hdds.container.balancer.size.leaving.source.max should be less "
+              + "than or equal to hdds.container.balancer.size.moved.max.per"
+              + ".iteration");
+    }
+
     // balancing interval should be greater than DUFactory refresh period
     DUFactory.Conf duConf = ozoneConfiguration.getObject(DUFactory.Conf.class);
     long refreshPeriod = duConf.getRefreshPeriod().toMillis();
@@ -510,6 +541,133 @@ private void 
validateConfiguration(ContainerBalancerConfiguration conf)
 
     validateNodeList(conf.getIncludeNodes(), "included");
     validateNodeList(conf.getExcludeNodes(), "excluded");
+    validateIncludeExcludeLists(conf);
+    validateIncludeContainersExist(conf);
+    validateEligibleDatanodePool(conf);
+  }
+
+  /**
+   * Rejects include lists that are fully covered by the corresponding exclude
+   * lists, which would leave no datanodes or containers to balance.
+   */
+  private void validateIncludeExcludeLists(ContainerBalancerConfiguration conf)
+      throws InvalidContainerBalancerConfigurationException {
+    Set<String> includeNodes = conf.getIncludeNodes();
+    Set<String> excludeNodes = conf.getExcludeNodes();
+    if (!includeNodes.isEmpty() && !excludeNodes.isEmpty()) {
+      boolean allIncludedNodesExcluded = true;
+      for (String includedNode : includeNodes) {
+        if (!isIncludedNodeExcluded(includedNode, excludeNodes)) {
+          allIncludedNodesExcluded = false;
+          break;
+        }
+      }
+      if (allIncludedNodesExcluded) {
+        throw new InvalidContainerBalancerConfigurationException(
+            "include-datanodes is a subset of exclude-datanodes, no datanode 
can participate in balancing.");
+      }
+    }
+
+    Set<ContainerID> includeContainers = conf.getIncludeContainers();
+    Set<ContainerID> excludeContainers = conf.getExcludeContainers();
+    if (!includeContainers.isEmpty() && 
excludeContainers.containsAll(includeContainers)) {
+      throw new InvalidContainerBalancerConfigurationException(
+          "include-containers is a subset of exclude-containers, no container 
can be selected for balancing.");
+    }
+  }
+
+  private boolean isIncludedNodeExcluded(String includedNode, Set<String> 
excludeNodes) {
+    if (excludeNodes.contains(includedNode)) {
+      return true;
+    }
+    for (DatanodeDetails dn : 
scm.getScmNodeManager().getNodesByAddress(includedNode)) {
+      if (excludeNodes.contains(dn.getHostName()) || 
excludeNodes.contains(dn.getIpAddress())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Rejects non-empty include-containers lists when any listed container ID
+   * does not exist in SCM.
+   */
+  private void validateIncludeContainersExist(ContainerBalancerConfiguration 
conf)
+      throws InvalidContainerBalancerConfigurationException {
+    Set<ContainerID> includeContainers = conf.getIncludeContainers();
+    if (includeContainers.isEmpty()) {
+      return;
+    }
+
+    ContainerManager containerManager = scm.getContainerManager();
+    List<ContainerID> missingContainers = new ArrayList<>();
+    for (ContainerID containerID : includeContainers) {
+      try {
+        containerManager.getContainer(containerID);
+      } catch (ContainerNotFoundException e) {
+        missingContainers.add(containerID);
+      }
+    }
+
+    if (!missingContainers.isEmpty()) {
+      throw new InvalidContainerBalancerConfigurationException(
+          "Container Balancer cannot start: included container ID(s) " + 
missingContainers
+              + " do not exist in SCM.");
+    }
+  }
+
+  /**
+   * Validates that enough healthy, in-service datanodes are eligible and that
+   * {@link 
ContainerBalancerConfiguration#getMaxDatanodesRatioToInvolvePerIteration()}
+   * allows at least one source and one target datanode per iteration.
+   */
+  private void validateEligibleDatanodePool(ContainerBalancerConfiguration 
conf)
+      throws InvalidContainerBalancerConfigurationException {
+    int eligibleCount = countEligibleDatanodes(conf);
+    if (eligibleCount < 2) {
+      throw new InvalidContainerBalancerConfigurationException(String.format(
+          "Container Balancer found %d eligible datanode(s) but requires at 
least 2.",
+          eligibleCount));
+    }
+    int maxDatanodesToInvolve = 
conf.computeMaxDatanodesToInvolvePerIteration(eligibleCount);
+    if (maxDatanodesToInvolve < 2) {
+      throw new InvalidContainerBalancerConfigurationException(String.format(
+          "max-datanodes-percentage-to-involve-per-iteration=%d allows at most 
"
+              + "%d datanode(s) per iteration with %d eligible datanode(s), "
+              + "but at least 2 are required for a source and target datanode "
+              + "pair.",
+          conf.getMaxDatanodesPercentageToInvolvePerIteration(),
+          maxDatanodesToInvolve, eligibleCount));
+    }
+  }
+
+  /**
+   * Counts healthy, in-service datanodes that can participate in balancing 
after
+   * applying include/exclude datanode configuration.
+   */
+  private int countEligibleDatanodes(ContainerBalancerConfiguration conf) {
+    Set<String> excludeNodes = conf.getExcludeNodes();
+    Set<String> includeNodes = conf.getIncludeNodes();
+    List<DatanodeInfo> healthyNodes = 
scm.getScmNodeManager().getNodes(IN_SERVICE, HEALTHY);
+    int eligibleCount = 0;
+    for (DatanodeDetails datanode : healthyNodes) {
+      if (!shouldExcludeDatanode(datanode, excludeNodes, includeNodes)) {
+        eligibleCount++;
+      }
+    }
+    return eligibleCount;
+  }
+
+  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;
   }
 
   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 38554253bab..bb2b4c87325 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
@@ -85,7 +85,6 @@ public class ContainerBalancerTask implements Runnable {
   private ContainerBalancer containerBalancer;
   private final SCMContext scmContext;
   private int totalNodesInCluster;
-  private double maxDatanodesRatioToInvolvePerIteration;
   private long maxSizeToMovePerIteration;
   private int countDatanodesInvolvedPerIteration;
   private long sizeScheduledForMoveInLatestIteration;
@@ -537,8 +536,6 @@ private boolean initializeIteration() {
       return false;
     }
 
-    this.maxDatanodesRatioToInvolvePerIteration =
-        config.getMaxDatanodesRatioToInvolvePerIteration();
     this.maxSizeToMovePerIteration = config.getMaxSizeToMovePerIteration();
 
     this.excludeNodes = config.getExcludeNodes();
@@ -939,7 +936,7 @@ private boolean reachedMaxSizeToMovePerIteration() {
   private boolean adaptWhenNearingIterationLimits() {
     // check if we're nearing max datanodes to involve
     int maxDatanodesToInvolve =
-        (int) (maxDatanodesRatioToInvolvePerIteration * totalNodesInCluster);
+        config.computeMaxDatanodesToInvolvePerIteration(totalNodesInCluster);
     if (countDatanodesInvolvedPerIteration + 1 == maxDatanodesToInvolve) {
       /* We're one datanode away from reaching the limit. Restrict potential
       targets to targets that have already been selected.
@@ -966,7 +963,7 @@ private boolean adaptWhenNearingIterationLimits() {
   private boolean adaptOnReachingIterationLimits() {
     // check if we've reached max datanodes to involve limit
     int maxDatanodesToInvolve =
-        (int) (maxDatanodesRatioToInvolvePerIteration * totalNodesInCluster);
+        config.computeMaxDatanodesToInvolvePerIteration(totalNodesInCluster);
     if (countDatanodesInvolvedPerIteration == maxDatanodesToInvolve) {
       // restrict both to already selected sources and targets
       findTargetStrategy.resetPotentialTargets(selectedTargets);
@@ -1172,14 +1169,7 @@ private List<DatanodeUsageInfo> getPotentialSources() {
    * @return true if Datanode should be excluded, else false
    */
   private boolean shouldExcludeDatanode(DatanodeDetails datanode) {
-    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 ContainerBalancer.shouldExcludeDatanode(datanode, excludeNodes, 
includeNodes);
   }
 
   /**
diff --git 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java
 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java
index 7324ae32b6d..acb3e64a3ca 100644
--- 
a/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java
+++ 
b/hadoop-hdds/server-scm/src/main/java/org/apache/hadoop/hdds/scm/server/SCMClientProtocolServer.java
@@ -1235,9 +1235,9 @@ public StartContainerBalancerResponseProto 
startContainerBalancer(
         int mdti = maxDatanodesPercentageToInvolvePerIteration.get();
         auditMap.put("maxDatanodesPercentageToInvolvePerIteration",
             String.valueOf(mdti));
-        if (mdti < 0 || mdti > 100) {
+        if (mdti <= 0 || mdti > 100) {
           throw new IOException("Max Datanodes Percentage To Involve Per 
Iteration" +
-                  "should be specified in the range [0, 100]");
+                  "should be specified in the range (0, 100]");
         }
         cbc.setMaxDatanodesPercentageToInvolvePerIteration(mdti);
       }
diff --git 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancer.java
 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancer.java
index 4864f235d5b..8931c9d6719 100644
--- 
a/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancer.java
+++ 
b/hadoop-hdds/server-scm/src/test/java/org/apache/hadoop/hdds/scm/container/balancer/TestContainerBalancer.java
@@ -19,6 +19,8 @@
 
 import static org.apache.hadoop.hdds.HddsConfigKeys.HDDS_NODE_REPORT_INTERVAL;
 import static 
org.apache.hadoop.hdds.HddsConfigKeys.HDDS_SCM_WAIT_TIME_AFTER_SAFE_MODE_EXIT;
+import static 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeOperationalState.IN_SERVICE;
+import static 
org.apache.hadoop.hdds.protocol.proto.HddsProtos.NodeState.HEALTHY;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -36,20 +38,27 @@
 
 import com.google.protobuf.ByteString;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
 import org.apache.hadoop.hdds.protocol.DatanodeDetails;
+import org.apache.hadoop.hdds.scm.container.ContainerID;
+import org.apache.hadoop.hdds.scm.container.ContainerManager;
+import org.apache.hadoop.hdds.scm.container.ContainerNotFoundException;
 import org.apache.hadoop.hdds.scm.ha.SCMContext;
 import org.apache.hadoop.hdds.scm.ha.SCMServiceManager;
 import org.apache.hadoop.hdds.scm.ha.StatefulServiceStateManager;
 import org.apache.hadoop.hdds.scm.ha.StatefulServiceStateManagerImpl;
+import org.apache.hadoop.hdds.scm.node.DatanodeInfo;
 import org.apache.hadoop.hdds.scm.node.NodeManager;
 import org.apache.hadoop.hdds.scm.node.states.NodeNotFoundException;
 import org.apache.hadoop.hdds.scm.server.StorageContainerManager;
+import org.apache.hadoop.ozone.OzoneConsts;
 import org.apache.ozone.test.GenericTestUtils;
 import org.apache.ozone.test.GenericTestUtils.LogCapturer;
 import org.junit.jupiter.api.BeforeEach;
@@ -67,6 +76,7 @@ public class TestContainerBalancer {
 
   private ContainerBalancer containerBalancer;
   private StorageContainerManager scm;
+  private NodeManager nodeManager;
   private ContainerBalancerConfiguration balancerConfiguration;
   private Map<String, ByteString> serviceToConfigMap = new HashMap<>();
   private OzoneConfiguration conf;
@@ -95,6 +105,9 @@ public void setup() throws IOException, 
NodeNotFoundException,
     GenericTestUtils.setLogLevel(ContainerBalancer.class, Level.DEBUG);
 
     when(scm.getScmNodeManager()).thenReturn(mock(NodeManager.class));
+    nodeManager = scm.getScmNodeManager();
+    List<DatanodeInfo> eligibleDatanodes = createEligibleDatanodes(10);
+    when(nodeManager.getNodes(IN_SERVICE, 
HEALTHY)).thenReturn(eligibleDatanodes);
     when(scm.getScmContext()).thenReturn(SCMContext.emptyContext());
     when(scm.getConfiguration()).thenReturn(conf);
     when(scm.getStatefulServiceStateManager()).thenReturn(serviceStateManager);
@@ -296,12 +309,11 @@ public void testGetBalancerStatusInfo() throws Exception {
 
   @Test
   public void testStartBalancerWithInvalidNodes() throws Exception {
-    NodeManager nm = scm.getScmNodeManager();
     String validHost = "1.2.3.4";
     String invalidHost = "invalid-host-name";
 
-    
when(nm.getNodesByAddress(invalidHost)).thenReturn(Collections.emptyList());
-    
when(nm.getNodesByAddress(validHost)).thenReturn(Collections.singletonList(mock(DatanodeDetails.class)));
+    
when(nodeManager.getNodesByAddress(invalidHost)).thenReturn(Collections.emptyList());
+    
when(nodeManager.getNodesByAddress(validHost)).thenReturn(Collections.singletonList(mock(DatanodeDetails.class)));
 
     // Test invalid includeNodes
     balancerConfiguration.setIncludeNodes(invalidHost);
@@ -321,7 +333,7 @@ public void testStartBalancerWithInvalidNodes() throws 
Exception {
 
     // Test a valid case
     balancerConfiguration.setExcludeNodes("");
-    balancerConfiguration.setIncludeNodes(validHost);
+    balancerConfiguration.setIncludeNodes("");
     assertDoesNotThrow(() -> startBalancer(balancerConfiguration));
     assertSame(ContainerBalancerTask.Status.RUNNING, 
containerBalancer.getBalancerStatus());
 
@@ -371,6 +383,93 @@ public void testGetBalancerStatusInfoAfterScmStop() throws 
Exception {
     assertNotNull(statusInfo.getStoppedAt());
   }
 
+  /**
+   * Tests new startup validation for conflicting include/exclude lists.
+   */
+  @Test
+  public void testRejectConflictingIncludeExcludeLists() throws Exception {
+    when(nodeManager.getNodesByAddress("dn0"))
+        .thenReturn(Collections.singletonList(mock(DatanodeDetails.class)));
+    when(nodeManager.getNodesByAddress("dn1"))
+        .thenReturn(Collections.singletonList(mock(DatanodeDetails.class)));
+    balancerConfiguration.setIncludeNodes("dn0,dn1");
+    balancerConfiguration.setExcludeNodes("dn0,dn1");
+
+    InvalidContainerBalancerConfigurationException ex =
+        assertThrows(InvalidContainerBalancerConfigurationException.class,
+            () -> containerBalancer.startBalancer(balancerConfiguration));
+    assertThat(ex.getMessage()).contains(
+        "include-datanodes is a subset of exclude-datanodes");
+    assertSame(ContainerBalancerTask.Status.STOPPED, 
containerBalancer.getBalancerStatus());
+
+    balancerConfiguration.setIncludeNodes("");
+    balancerConfiguration.setExcludeNodes("");
+    balancerConfiguration.setIncludeContainers("1, 2");
+    balancerConfiguration.setExcludeContainers("1,2,3");
+    ex = assertThrows(InvalidContainerBalancerConfigurationException.class,
+        () -> containerBalancer.startBalancer(balancerConfiguration));
+    assertThat(ex.getMessage()).contains(
+        "include-containers is a subset of exclude-containers");
+    assertSame(ContainerBalancerTask.Status.STOPPED, 
containerBalancer.getBalancerStatus());
+  }
+
+  /**
+   * Tests new startup validation for include-containers, datanode pool, and
+   * size limits.
+   */
+  @Test
+  public void testRejectInvalidStartupConfiguration() throws Exception {
+    ContainerManager containerManager = mock(ContainerManager.class);
+    when(scm.getContainerManager()).thenReturn(containerManager);
+    when(containerManager.getContainer(any(ContainerID.class)))
+        .thenThrow(new ContainerNotFoundException(ContainerID.valueOf(1)));
+
+    balancerConfiguration.setIncludeContainers("1");
+    InvalidContainerBalancerConfigurationException ex =
+        assertThrows(InvalidContainerBalancerConfigurationException.class,
+            () -> containerBalancer.startBalancer(balancerConfiguration));
+    assertThat(ex.getMessage()).contains("do not exist in SCM");
+    assertSame(ContainerBalancerTask.Status.STOPPED, 
containerBalancer.getBalancerStatus());
+
+    balancerConfiguration.setIncludeContainers("");
+    List<DatanodeInfo> fiveDatanodes = createEligibleDatanodes(5);
+    when(nodeManager.getNodes(IN_SERVICE, HEALTHY)).thenReturn(fiveDatanodes);
+    balancerConfiguration.setMaxDatanodesPercentageToInvolvePerIteration(20);
+    ex = assertThrows(InvalidContainerBalancerConfigurationException.class,
+        () -> containerBalancer.startBalancer(balancerConfiguration));
+    assertThat(ex.getMessage()).contains("at least 2 are required for a source 
and target datanode pair.");
+    assertSame(ContainerBalancerTask.Status.STOPPED, 
containerBalancer.getBalancerStatus());
+
+    balancerConfiguration.setMaxSizeToMovePerIteration(100 * OzoneConsts.GB);
+    balancerConfiguration.setMaxSizeEnteringTarget(200 * OzoneConsts.GB);
+    ex = assertThrows(InvalidContainerBalancerConfigurationException.class,
+        () -> containerBalancer.startBalancer(balancerConfiguration));
+    assertThat(ex.getMessage()).contains(
+        "hdds.container.balancer.size.entering.target.max should be less than 
or "
+            + "equal to hdds.container.balancer.size.moved.max.per.iteration");
+    assertSame(ContainerBalancerTask.Status.STOPPED, 
containerBalancer.getBalancerStatus());
+
+    balancerConfiguration.setMaxSizeEnteringTarget(100 * OzoneConsts.GB);
+    balancerConfiguration.setMaxSizeLeavingSource(200 * OzoneConsts.GB);
+    ex = assertThrows(InvalidContainerBalancerConfigurationException.class,
+        () -> containerBalancer.startBalancer(balancerConfiguration));
+    assertThat(ex.getMessage()).contains(
+        "hdds.container.balancer.size.leaving.source.max should be less than 
or "
+            + "equal to hdds.container.balancer.size.moved.max.per.iteration");
+    assertSame(ContainerBalancerTask.Status.STOPPED, 
containerBalancer.getBalancerStatus());
+  }
+
+  private static List<DatanodeInfo> createEligibleDatanodes(int count) {
+    List<DatanodeInfo> datanodes = new ArrayList<>(count);
+    for (int i = 0; i < count; i++) {
+      DatanodeInfo datanode = mock(DatanodeInfo.class);
+      when(datanode.getHostName()).thenReturn("dn" + i);
+      when(datanode.getIpAddress()).thenReturn("10.0.0." + i);
+      datanodes.add(datanode);
+    }
+    return datanodes;
+  }
+
   private void startBalancer(ContainerBalancerConfiguration config)
       throws IllegalContainerBalancerStateException, IOException,
       InvalidContainerBalancerConfigurationException, TimeoutException {
diff --git 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerStartSubcommand.java
 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerStartSubcommand.java
index 2cab942f618..09de9e2b758 100644
--- 
a/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerStartSubcommand.java
+++ 
b/hadoop-ozone/cli-admin/src/main/java/org/apache/hadoop/hdds/scm/cli/ContainerBalancerStartSubcommand.java
@@ -143,10 +143,8 @@ public void execute(ScmClient scmClient) throws 
IOException {
       System.out.println("Container Balancer started successfully.");
     } else {
       String reason = "";
-      System.err.println("Failed to start Container Balancer.");
       if (response.hasMessage()) {
         reason = response.getMessage();
-        System.err.printf("Failure reason: %s%n", reason);
       }
       throw new IOException("Failed to start Container Balancer. " + reason);
     }
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 613ca37e586..407785d48a3 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
@@ -596,8 +596,8 @@ public void 
testContainerBalancerStartSubcommandWhenBalancerIsRunning()
             .setStart(false)
             .setMessage("")
             .build());
-    assertThrows(IOException.class, () -> startCmd.execute(scmClient));
-    assertThat(err.get()).containsPattern(FAILED_TO_START);
+    IOException ex = assertThrows(IOException.class, () -> 
startCmd.execute(scmClient));
+    assertThat(ex.getMessage()).containsPattern(FAILED_TO_START);
   }
 
   @Test
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/TestFailoverWithSCMHA.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/TestFailoverWithSCMHA.java
index bbbee76cede..c2c7b3c6e23 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/TestFailoverWithSCMHA.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/hdds/scm/TestFailoverWithSCMHA.java
@@ -171,6 +171,7 @@ public void 
testContainerBalancerPersistsConfigurationInAllSCMs()
     ScmClient scmClient = new ContainerOperationClient(conf);
     // assert that container balancer is not running right now
     assertFalse(scmClient.getContainerBalancerStatus());
+    
conf.setInt("hdds.container.balancer.datanodes.involved.max.percentage.per.iteration",
 100);
     ContainerBalancerConfiguration balancerConf =
         conf.getObject(ContainerBalancerConfiguration.class);
     ContainerBalancer containerBalancer = leader.getContainerBalancer();
diff --git 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestContainerBalancerOperations.java
 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestContainerBalancerOperations.java
index bb8d0704538..80d2c11079f 100644
--- 
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestContainerBalancerOperations.java
+++ 
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/TestContainerBalancerOperations.java
@@ -25,17 +25,21 @@
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.hadoop.hdds.conf.OzoneConfiguration;
+import org.apache.hadoop.hdds.protocol.proto.HddsProtos;
 import org.apache.hadoop.hdds.scm.PlacementPolicy;
 import org.apache.hadoop.hdds.scm.ScmConfigKeys;
 import org.apache.hadoop.hdds.scm.cli.ContainerOperationClient;
 import org.apache.hadoop.hdds.scm.client.ScmClient;
 import org.apache.hadoop.hdds.scm.container.ContainerID;
 import 
org.apache.hadoop.hdds.scm.container.balancer.ContainerBalancerConfiguration;
+import 
org.apache.hadoop.hdds.scm.container.common.helpers.ContainerWithPipeline;
 import 
org.apache.hadoop.hdds.scm.container.placement.algorithms.SCMContainerPlacementCapacity;
 import org.apache.ozone.test.GenericTestUtils;
 import org.junit.jupiter.api.AfterAll;
@@ -84,7 +88,7 @@ public void testContainerBalancerCLIOperations() throws 
Exception {
     Optional<Integer> iterations = Optional.of(10000);
     Optional<Integer> maxDatanodesPercentageToInvolvePerIteration =
         Optional.of(100);
-    Optional<Long> maxSizeToMovePerIterationInGB = Optional.of(1L);
+    Optional<Long> maxSizeToMovePerIterationInGB = Optional.of(6L);
     Optional<Long> maxSizeEnteringTargetInGB = Optional.of(6L);
     Optional<Long> maxSizeLeavingSourceInGB = Optional.of(6L);
     Optional<Integer> balancingInterval = Optional.of(70);
@@ -149,14 +153,24 @@ public void testIfCBCLIOverridesConfigs() throws 
Exception {
     //CLI option for iterations and balancing interval is not passed
     Optional<Integer> iterations = Optional.empty();
     Optional<Integer> balancingInterval = Optional.empty();
-    String excludedContainersList = "1,2,3";
-    String includedContainersList = "4,5";
+    List<ContainerWithPipeline> createdContainers = new ArrayList<>(5);
+    for (int i = 0; i < 5; i++) {
+      createdContainers.add(containerBalancerClient.createContainer(
+          HddsProtos.ReplicationType.RATIS,
+          HddsProtos.ReplicationFactor.ONE,
+          OzoneConsts.OZONE));
+    }
+    String excludedContainersList = 
createdContainers.get(0).getContainerInfo().getContainerID() + ","
+        + createdContainers.get(1).getContainerInfo().getContainerID() + ","
+        + createdContainers.get(2).getContainerInfo().getContainerID();
+    String includedContainersList = 
createdContainers.get(3).getContainerInfo().getContainerID() + ","
+        + createdContainers.get(4).getContainerInfo().getContainerID();
 
     //CLI options are passed
     Optional<Double> threshold = Optional.of(0.1);
     Optional<Integer> maxDatanodesPercentageToInvolvePerIteration =
             Optional.of(100);
-    Optional<Long> maxSizeToMovePerIterationInGB = Optional.of(1L);
+    Optional<Long> maxSizeToMovePerIterationInGB = Optional.of(6L);
     Optional<Long> maxSizeEnteringTargetInGB = Optional.of(6L);
     Optional<Long> maxSizeLeavingSourceInGB = Optional.of(6L);
     Optional<Integer> moveTimeout = Optional.of(65);
@@ -212,7 +226,7 @@ public void testStopBalancerIdempotent() throws IOException 
{
     Optional<Integer> iterations = Optional.of(10000);
     Optional<Integer> maxDatanodesPercentageToInvolvePerIteration =
         Optional.of(100);
-    Optional<Long> maxSizeToMovePerIterationInGB = Optional.of(1L);
+    Optional<Long> maxSizeToMovePerIterationInGB = Optional.of(6L);
     Optional<Long> maxSizeEnteringTargetInGB = Optional.of(6L);
     Optional<Long> maxSizeLeavingSourceInGB = Optional.of(6L);
     Optional<Integer> balancingInterval = Optional.of(70);


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

Reply via email to