You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@helix.apache.org by ji...@apache.org on 2020/08/13 18:22:56 UTC

[helix] branch master updated: Do not ignore any map or list field key change in the Change Detector. (#1256)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 096b074  Do not ignore any map or list field key change in the Change Detector. (#1256)
096b074 is described below

commit 096b074c56af7a7ad79dbc780c5a08c04701083a
Author: Jiajun Wang <jj...@linkedin.com>
AuthorDate: Thu Aug 13 11:22:45 2020 -0700

    Do not ignore any map or list field key change in the Change Detector. (#1256)
    
    This PR extends the scope of the non-trimmable elements that are defined in the ResourceChangeDetector. With this change, any key changes in the Map fields or List fields won't be ignored by the change detector.
    
    Instead of just fixing the issue that modifying the IS preference list does not trigger automatic rebalance, this change will also help to prevent other potential bugs with the same root cause.
    
    Note this change might increase the rebalancer running in some circumstances. However, given a very explicit allowlist (or blocklist) in the change detector is very expensive to maintain, the current solution trades off the potential performance impact for simplicity.
---
 .../trimmer/HelixPropertyTrimmer.java              | 100 ++++++++++-----
 .../changedetector/trimmer/IdealStateTrimmer.java  |   5 +-
 .../trimmer/TestHelixPropoertyTimmer.java          | 134 ++++++++++++++-------
 .../WagedRebalancer/TestWagedRebalance.java        |  14 +++
 4 files changed, 180 insertions(+), 73 deletions(-)

diff --git a/helix-core/src/main/java/org/apache/helix/controller/changedetector/trimmer/HelixPropertyTrimmer.java b/helix-core/src/main/java/org/apache/helix/controller/changedetector/trimmer/HelixPropertyTrimmer.java
index 8ae585d..dbab09c 100644
--- a/helix-core/src/main/java/org/apache/helix/controller/changedetector/trimmer/HelixPropertyTrimmer.java
+++ b/helix-core/src/main/java/org/apache/helix/controller/changedetector/trimmer/HelixPropertyTrimmer.java
@@ -19,19 +19,20 @@ package org.apache.helix.controller.changedetector.trimmer;
  * under the License.
  */
 
-import java.util.List;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 
 import org.apache.helix.HelixProperty;
 import org.apache.helix.zookeeper.datamodel.ZNRecord;
 
+
 /**
  * An abstract class that contains the common logic to trim HelixProperty by removing unnecessary
  * fields.
  */
 public abstract class HelixPropertyTrimmer<T extends HelixProperty> {
-
   /**
    * The possible Helix-aware field types in the HelixProperty.
    */
@@ -41,56 +42,97 @@ public abstract class HelixPropertyTrimmer<T extends HelixProperty> {
 
   /**
    * @param property
-   * @return a map contains the field keys of all non-trimmable fields that need to be kept.
+   * @return a map contains the field keys of all non-trimmable field values that need to be kept.
    */
   protected abstract Map<FieldType, Set<String>> getNonTrimmableFields(T property);
 
   /**
+   * By default, ensure the keys of all map fields and list fields are preserved even after the
+   * trim. The keys of list and map fields are relatively stable and contain important information.
+   * So they should not be trimmed.
+   * Extend to override this behavior if necessary.
+   * @param property
+   * @return a map contains all non-trimmable field keys that need to be kept.
+   *         Note that the values will be trimmed.
+   */
+  protected Map<FieldType, Set<String>> getNonTrimmableKeys(T property) {
+    Map<FieldType, Set<String>> nonTrimmableKeys = new HashMap<>();
+    nonTrimmableKeys.put(FieldType.MAP_FIELD, property.getRecord().getMapFields().keySet());
+    nonTrimmableKeys.put(FieldType.LIST_FIELD, property.getRecord().getListFields().keySet());
+    return nonTrimmableKeys;
+  }
+
+  /**
    * @param property
    * @return a copy of the property that has been trimmed.
    */
   public abstract T trimProperty(T property);
 
+  // TODO: Simplify or remove the trim logic when we clearly separate the input and output ZNode.
+  // TODO: e.g. Resource Config for user input and the Ideal State for the Helix output.
   /**
    * Return a ZNrecord as the trimmed copy of the original property.
    * Note that we are NOT doing deep copy to avoid performance impact.
    * @param originalProperty
    */
   protected ZNRecord doTrim(T originalProperty) {
+    ZNRecord originalZNRecord = originalProperty.getRecord();
     ZNRecord trimmedZNRecord = new ZNRecord(originalProperty.getId());
-    for (Map.Entry<FieldType, Set<String>> fieldEntry : getNonTrimmableFields(
-        originalProperty).entrySet()) {
+
+    // Copy the non-trimmable values to the trimmed record.
+    // Note to copy the values first. Or if the key-only copy happens first, the value copy will
+    // skip to avoid implicit overwrite.
+    copyNonTrimmableInfo(originalZNRecord, trimmedZNRecord, getNonTrimmableFields(originalProperty),
+        false);
+    // Copy the non-trimmable keys (ignore the values) to the trimmed record.
+    copyNonTrimmableInfo(originalZNRecord, trimmedZNRecord, getNonTrimmableKeys(originalProperty),
+        true);
+    return trimmedZNRecord;
+  }
+
+  /**
+   * Copy the non trimmable information to the trimmed ZNRecord.
+   * @param originalZNRecord
+   * @param trimmedZNRecord
+   * @param nonTrimmableFields
+   * @param trimValue if true, the value will not be copied. Only the keys will be kept.
+   */
+  private void copyNonTrimmableInfo(ZNRecord originalZNRecord, ZNRecord trimmedZNRecord,
+      Map<FieldType, Set<String>> nonTrimmableFields, boolean trimValue) {
+    for (Map.Entry<FieldType, Set<String>> fieldEntry : nonTrimmableFields.entrySet()) {
       FieldType fieldType = fieldEntry.getKey();
       Set<String> fieldKeySet = fieldEntry.getValue();
       if (null == fieldKeySet || fieldKeySet.isEmpty()) {
         continue;
       }
       switch (fieldType) {
-      case SIMPLE_FIELD:
-        fieldKeySet.stream().forEach(fieldKey -> {
-          String value = originalProperty.getRecord().getSimpleField(fieldKey);
-          if (null != value) {
-            trimmedZNRecord.setSimpleField(fieldKey, value);
-          }
-        });
-      case LIST_FIELD:
-        fieldKeySet.stream().forEach(fieldKey -> {
-          List<String> values = originalProperty.getRecord().getListField(fieldKey);
-          if (null != values) {
-            trimmedZNRecord.setListField(fieldKey, values);
-          }
-        });
-      case MAP_FIELD:
-        fieldKeySet.stream().forEach(fieldKey -> {
-          Map<String, String> valueMap = originalProperty.getRecord().getMapField(fieldKey);
-          if (null != valueMap) {
-            trimmedZNRecord.setMapField(fieldKey, valueMap);
-          }
-        });
-      default:
-        break;
+        case SIMPLE_FIELD:
+          fieldKeySet.stream().forEach(fieldKey -> {
+            if (originalZNRecord.getSimpleFields().containsKey(fieldKey)) {
+              trimmedZNRecord.getSimpleFields().putIfAbsent(fieldKey,
+                  trimValue ? null : originalZNRecord.getSimpleField(fieldKey));
+            }
+          });
+          break;
+        case LIST_FIELD:
+          fieldKeySet.stream().forEach(fieldKey -> {
+            if (originalZNRecord.getListFields().containsKey(fieldKey)) {
+              trimmedZNRecord.getListFields().putIfAbsent(fieldKey,
+                  trimValue ? Collections.EMPTY_LIST : originalZNRecord.getListField(fieldKey));
+            }
+          });
+          break;
+        case MAP_FIELD:
+          fieldKeySet.stream().forEach(fieldKey -> {
+            if (originalZNRecord.getMapFields().containsKey(fieldKey)) {
+              trimmedZNRecord.getMapFields().putIfAbsent(fieldKey,
+                  trimValue ? Collections.EMPTY_MAP : originalZNRecord.getMapField(fieldKey));
+            }
+          });
+          break;
+        default:
+          break;
       }
     }
-    return trimmedZNRecord;
   }
 }
diff --git a/helix-core/src/main/java/org/apache/helix/controller/changedetector/trimmer/IdealStateTrimmer.java b/helix-core/src/main/java/org/apache/helix/controller/changedetector/trimmer/IdealStateTrimmer.java
index 8c2ff5a..4132110 100644
--- a/helix-core/src/main/java/org/apache/helix/controller/changedetector/trimmer/IdealStateTrimmer.java
+++ b/helix-core/src/main/java/org/apache/helix/controller/changedetector/trimmer/IdealStateTrimmer.java
@@ -73,10 +73,9 @@ public class IdealStateTrimmer extends HelixPropertyTrimmer<IdealState> {
     // They are fixed and considered as part of the cluster topology.
     switch (idealState.getRebalanceMode()) {
     case CUSTOMIZED:
-      // For CUSTOMZIED resources, both list and map fields are user configured partition state
-      // assignment. So they are not trimmable.
+      // For CUSTOMZIED resources, map fields are user configured partition state assignment. So
+      // they are not trimmable.
       nonTrimmableFields.put(FieldType.MAP_FIELD, idealState.getRecord().getMapFields().keySet());
-      nonTrimmableFields.put(FieldType.LIST_FIELD, idealState.getRecord().getListFields().keySet());
       break;
     case SEMI_AUTO:
       // For SEMI_AUTO resources, list fields are user configured partition placement. So it is not
diff --git a/helix-core/src/test/java/org/apache/helix/controller/changedetector/trimmer/TestHelixPropoertyTimmer.java b/helix-core/src/test/java/org/apache/helix/controller/changedetector/trimmer/TestHelixPropoertyTimmer.java
index 7e881f4..8067931 100644
--- a/helix-core/src/test/java/org/apache/helix/controller/changedetector/trimmer/TestHelixPropoertyTimmer.java
+++ b/helix-core/src/test/java/org/apache/helix/controller/changedetector/trimmer/TestHelixPropoertyTimmer.java
@@ -19,6 +19,7 @@ package org.apache.helix.controller.changedetector.trimmer;
  * under the License.
  */
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -42,10 +43,12 @@ import org.testng.annotations.Test;
 
 import static org.mockito.Mockito.when;
 
+
 public class TestHelixPropoertyTimmer {
   private final String CLUSTER_NAME = "CLUSTER";
   private final String INSTANCE_NAME = "INSTANCE";
   private final String RESOURCE_NAME = "RESOURCE";
+  private final String PARTITION_NAME = "DEFAULT_PARTITION";
 
   private final Set<HelixConstants.ChangeType> _changeTypes = new HashSet<>();
   private final Map<String, InstanceConfig> _instanceConfigMap = new HashMap<>();
@@ -66,18 +69,41 @@ public class TestHelixPropoertyTimmer {
     _changeTypes.add(HelixConstants.ChangeType.RESOURCE_CONFIG);
     _changeTypes.add(HelixConstants.ChangeType.CLUSTER_CONFIG);
 
-    _instanceConfigMap.put(INSTANCE_NAME, new InstanceConfig(INSTANCE_NAME));
+    InstanceConfig instanceConfig = new InstanceConfig(INSTANCE_NAME);
+    instanceConfig.setInstanceEnabledForPartition(RESOURCE_NAME, PARTITION_NAME, false);
+    fillKeyValues(instanceConfig);
+    _instanceConfigMap.put(INSTANCE_NAME, instanceConfig);
+
     IdealState idealState = new IdealState(RESOURCE_NAME);
     idealState.setRebalanceMode(IdealState.RebalanceMode.FULL_AUTO);
+    idealState.setPreferenceList(PARTITION_NAME, new ArrayList<>());
+    idealState.setPartitionState(PARTITION_NAME, INSTANCE_NAME, "LEADER");
+    fillKeyValues(idealState);
     _idealStateMap.put(RESOURCE_NAME, idealState);
-    _resourceConfigMap.put(RESOURCE_NAME, new ResourceConfig(RESOURCE_NAME));
+
+    ResourceConfig resourceConfig = new ResourceConfig(RESOURCE_NAME);
+    Map<String, List<String>> configPreferenceList = new HashMap<>();
+    configPreferenceList.put(PARTITION_NAME, new ArrayList<>());
+    resourceConfig.setPreferenceLists(configPreferenceList);
+    fillKeyValues(resourceConfig);
+    _resourceConfigMap.put(RESOURCE_NAME, resourceConfig);
+
     _clusterConfig = new ClusterConfig(CLUSTER_NAME);
+    fillKeyValues(_clusterConfig);
 
     _dataProvider =
         getMockDataProvider(_changeTypes, _instanceConfigMap, _idealStateMap, _resourceConfigMap,
             _clusterConfig);
   }
 
+  // Fill the testing helix property to ensure that we have at least one sample in every types of data.
+  private void fillKeyValues(HelixProperty helixProperty) {
+    helixProperty.getRecord().setSimpleField("MockFieldKey", "MockValue");
+    helixProperty.getRecord()
+        .setMapField("MockFieldKey", Collections.singletonMap("MockKey", "MockValue"));
+    helixProperty.getRecord().setListField("MockFieldKey", Collections.singletonList("MockValue"));
+  }
+
   private ResourceControllerDataProvider getMockDataProvider(
       Set<HelixConstants.ChangeType> changeTypes, Map<String, InstanceConfig> instanceConfigMap,
       Map<String, IdealState> idealStateMap, Map<String, ResourceConfig> resourceConfigMap,
@@ -110,6 +136,9 @@ public class TestHelixPropoertyTimmer {
           IdealStateTrimmer.getInstance().getNonTrimmableFields(idealState), idealState,
           HelixConstants.ChangeType.IDEAL_STATE, detector, _dataProvider);
 
+      modifyListMapfieldKeysAndVerifyDetector(idealState, HelixConstants.ChangeType.IDEAL_STATE,
+          detector, _dataProvider);
+
       // Additional test to ensure Ideal State map/list fields are detected correctly according to
       // the rebalance mode.
       // For the following test, we can only focus on the smaller scope defined by the following map.
@@ -119,38 +148,36 @@ public class TestHelixPropoertyTimmer {
       idealState.setRebalanceMode(IdealState.RebalanceMode.SEMI_AUTO);
       // refresh the detector cache after modification to avoid unexpected change detected.
       detector.updateSnapshots(_dataProvider);
-      overwriteFieldMap.put(FieldType.LIST_FIELD, Collections.singleton("partitionList_SEMI_AUTO"));
+      overwriteFieldMap.put(FieldType.LIST_FIELD, Collections.singleton(PARTITION_NAME));
       changeNonTrimmableValuesAndVerifyDetector(overwriteFieldMap, idealState,
           HelixConstants.ChangeType.IDEAL_STATE, detector, _dataProvider);
 
-      // CUSTOMZIED: List and Map fields are non-trimmable
+      // CUSTOMZIED: Map fields are non-trimmable
       idealState.setRebalanceMode(IdealState.RebalanceMode.CUSTOMIZED);
       // refresh the detector cache after modification to avoid unexpected change detected.
       detector.updateSnapshots(_dataProvider);
       overwriteFieldMap.clear();
-      overwriteFieldMap
-          .put(FieldType.LIST_FIELD, Collections.singleton("partitionList_CUSTOMIZED"));
-      overwriteFieldMap.put(FieldType.MAP_FIELD, Collections.singleton("partitionMap_CUSTOMIZED"));
+      overwriteFieldMap.put(FieldType.MAP_FIELD, Collections.singleton(PARTITION_NAME));
       changeNonTrimmableValuesAndVerifyDetector(overwriteFieldMap, idealState,
           HelixConstants.ChangeType.IDEAL_STATE, detector, _dataProvider);
     }
     // 3. Resource Config
     for (ResourceConfig resourceConfig : _resourceConfigMap.values()) {
-      // Add a non-trimmable preference list to the resource config, this change should be detected as well.
-      Map<String, List<String>> preferenceList = new HashMap<>();
-      preferenceList.put("partitionList_ResourceConfig", Collections.emptyList());
-      resourceConfig.setPreferenceLists(preferenceList);
-      // refresh the detector cache after modification to avoid unexpected change detected.
-      detector.updateSnapshots(_dataProvider);
       changeNonTrimmableValuesAndVerifyDetector(
           ResourceConfigTrimmer.getInstance().getNonTrimmableFields(resourceConfig), resourceConfig,
           HelixConstants.ChangeType.RESOURCE_CONFIG, detector, _dataProvider);
+
+      modifyListMapfieldKeysAndVerifyDetector(resourceConfig,
+          HelixConstants.ChangeType.RESOURCE_CONFIG, detector, _dataProvider);
     }
     // 4. Instance Config
     for (InstanceConfig instanceConfig : _instanceConfigMap.values()) {
       changeNonTrimmableValuesAndVerifyDetector(
           InstanceConfigTrimmer.getInstance().getNonTrimmableFields(instanceConfig), instanceConfig,
           HelixConstants.ChangeType.INSTANCE_CONFIG, detector, _dataProvider);
+
+      modifyListMapfieldKeysAndVerifyDetector(instanceConfig,
+          HelixConstants.ChangeType.INSTANCE_CONFIG, detector, _dataProvider);
     }
   }
 
@@ -176,21 +203,22 @@ public class TestHelixPropoertyTimmer {
       // refresh the detector cache after modification to avoid unexpected change detected.
       detector.updateSnapshots(_dataProvider);
       changeTrimmableValuesAndVerifyDetector(
-          new FieldType[] { FieldType.SIMPLE_FIELD, FieldType.MAP_FIELD }, idealState, detector,
+          new FieldType[]{FieldType.SIMPLE_FIELD, FieldType.MAP_FIELD}, idealState, detector,
           _dataProvider);
 
       // CUSTOMZIED: List and Map fields are non-trimmable
       idealState.setRebalanceMode(IdealState.RebalanceMode.CUSTOMIZED);
       // refresh the detector cache after modification to avoid unexpected change detected.
       detector.updateSnapshots(_dataProvider);
-      changeTrimmableValuesAndVerifyDetector(new FieldType[] { FieldType.SIMPLE_FIELD }, idealState,
-          detector, _dataProvider);
+      changeTrimmableValuesAndVerifyDetector(
+          new FieldType[]{FieldType.SIMPLE_FIELD, FieldType.LIST_FIELD}, idealState, detector,
+          _dataProvider);
     }
     // 3. Resource Config
     for (ResourceConfig resourceConfig : _resourceConfigMap.values()) {
       // Preference lists in the list fields are non-trimmable
       changeTrimmableValuesAndVerifyDetector(
-          new FieldType[] { FieldType.SIMPLE_FIELD, FieldType.MAP_FIELD }, resourceConfig, detector,
+          new FieldType[]{FieldType.SIMPLE_FIELD, FieldType.MAP_FIELD}, resourceConfig, detector,
           _dataProvider);
     }
     // 4. Instance Config
@@ -200,6 +228,21 @@ public class TestHelixPropoertyTimmer {
     }
   }
 
+  private void modifyListMapfieldKeysAndVerifyDetector(HelixProperty helixProperty,
+      HelixConstants.ChangeType expectedChangeType, ResourceChangeDetector detector,
+      ResourceControllerDataProvider dataProvider) {
+    helixProperty.getRecord()
+        .setListField(helixProperty.getId() + "NewListField", Collections.singletonList("foobar"));
+    helixProperty.getRecord()
+        .setMapField(helixProperty.getId() + "NewMapField", Collections.singletonMap("foo", "bar"));
+    detector.updateSnapshots(dataProvider);
+    for (HelixConstants.ChangeType changeType : HelixConstants.ChangeType.values()) {
+      Assert.assertEquals(detector.getChangesByType(changeType).size(),
+          changeType == expectedChangeType ? 1 : 0,
+          String.format("Any key changes in the List or Map fields shall be detected!"));
+    }
+  }
+
   private void changeNonTrimmableValuesAndVerifyDetector(
       Map<FieldType, Set<String>> nonTrimmableFieldMap, HelixProperty helixProperty,
       HelixConstants.ChangeType expectedChangeType, ResourceChangeDetector detector,
@@ -207,17 +250,17 @@ public class TestHelixPropoertyTimmer {
     for (FieldType type : nonTrimmableFieldMap.keySet()) {
       for (String fieldKey : nonTrimmableFieldMap.get(type)) {
         switch (type) {
-        case LIST_FIELD:
-          helixProperty.getRecord().setListField(fieldKey, Collections.singletonList("foobar"));
-          break;
-        case MAP_FIELD:
-          helixProperty.getRecord().setMapField(fieldKey, Collections.singletonMap("foo", "bar"));
-          break;
-        case SIMPLE_FIELD:
-          helixProperty.getRecord().setSimpleField(fieldKey, "foobar");
-          break;
-        default:
-          Assert.fail("Unknown field type " + type.name());
+          case LIST_FIELD:
+            helixProperty.getRecord().setListField(fieldKey, Collections.singletonList("foobar"));
+            break;
+          case MAP_FIELD:
+            helixProperty.getRecord().setMapField(fieldKey, Collections.singletonMap("foo", "bar"));
+            break;
+          case SIMPLE_FIELD:
+            helixProperty.getRecord().setSimpleField(fieldKey, "foobar");
+            break;
+          default:
+            Assert.fail("Unknown field type " + type.name());
         }
         detector.updateSnapshots(dataProvider);
         for (HelixConstants.ChangeType changeType : HelixConstants.ChangeType.values()) {
@@ -238,19 +281,28 @@ public class TestHelixPropoertyTimmer {
       ResourceControllerDataProvider dataProvider) {
     for (FieldType type : trimmableFieldTypes) {
       switch (type) {
-      case LIST_FIELD:
-        helixProperty.getRecord()
-            .setListField("TrimmableListField", Collections.singletonList("foobar"));
-        break;
-      case MAP_FIELD:
-        helixProperty.getRecord()
-            .setMapField("TrimmableMapField", Collections.singletonMap("foo", "bar"));
-        break;
-      case SIMPLE_FIELD:
-        helixProperty.getRecord().setSimpleField("TrimmableSimpleField", "foobar");
-        break;
-      default:
-        Assert.fail("Unknown field type " + type.name());
+        case LIST_FIELD:
+          // Modify value if key exists.
+          // Note if adding new keys, then the change will be detected regardless of the content.
+          helixProperty.getRecord().getListFields().keySet().stream().forEach(key -> {
+            helixProperty.getRecord().setListField(key,
+                Collections.singletonList("new-foobar" + System.currentTimeMillis()));
+          });
+          break;
+        case MAP_FIELD:
+          // Modify value if key exists.
+          // Note if adding new keys, then the change will be detected regardless of the content.
+          helixProperty.getRecord().getMapFields().keySet().stream().forEach(key -> {
+            helixProperty.getRecord().setMapField(key,
+                Collections.singletonMap("new-foo", "bar" + System.currentTimeMillis()));
+          });
+          break;
+        case SIMPLE_FIELD:
+          helixProperty.getRecord()
+              .setSimpleField("TrimmableSimpleField", "foobar" + System.currentTimeMillis());
+          break;
+        default:
+          Assert.fail("Unknown field type " + type.name());
       }
       detector.updateSnapshots(dataProvider);
       for (HelixConstants.ChangeType changeType : HelixConstants.ChangeType.values()) {
diff --git a/helix-core/src/test/java/org/apache/helix/integration/rebalancer/WagedRebalancer/TestWagedRebalance.java b/helix-core/src/test/java/org/apache/helix/integration/rebalancer/WagedRebalancer/TestWagedRebalance.java
index 5c1241b..c232462 100644
--- a/helix-core/src/test/java/org/apache/helix/integration/rebalancer/WagedRebalancer/TestWagedRebalance.java
+++ b/helix-core/src/test/java/org/apache/helix/integration/rebalancer/WagedRebalancer/TestWagedRebalance.java
@@ -20,6 +20,7 @@ package org.apache.helix.integration.rebalancer.WagedRebalancer;
  */
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -304,6 +305,19 @@ public class TestWagedRebalance extends ZkTestBase {
     ExternalView ev =
         _gSetupTool.getClusterManagementTool().getResourceExternalView(CLUSTER_NAME, dbName);
     Assert.assertEquals(ev.getPartitionSet().size(), PARTITIONS + 1);
+
+    // Customize the partition list instead of calling rebalance.
+    // So there is no other changes in the IdealState. The rebalancer shall still trigger
+    // new baseline calculation in this case.
+    is = _gSetupTool.getClusterManagementTool().getResourceIdealState(CLUSTER_NAME, dbName);
+    is.setPreferenceList(dbName + "_customizedPartition", Collections.EMPTY_LIST);
+    _gSetupTool.getClusterManagementTool().setResourceIdealState(CLUSTER_NAME, dbName, is);
+    Thread.sleep(300);
+
+    validate(newReplicaFactor);
+    ev =
+        _gSetupTool.getClusterManagementTool().getResourceExternalView(CLUSTER_NAME, dbName);
+    Assert.assertEquals(ev.getPartitionSet().size(), PARTITIONS + 2);
   }
 
   @Test(dependsOnMethods = "test")