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

xyuanlu 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 e7ada0907 Add cluster config update validation (#3026)
e7ada0907 is described below

commit e7ada0907d7dca67851fbfd60e46340087d8d53d
Author: Xiaxuan Gao <[email protected]>
AuthorDate: Sun May 4 23:01:33 2025 -0700

    Add cluster config update validation (#3026)
    
    Add cluster config update validation
---
 .../server/resources/helix/ClusterAccessor.java    | 58 +++++++++++++++++++
 .../helix/rest/server/TestClusterAccessor.java     | 66 ++++++++++++++++++++++
 2 files changed, 124 insertions(+)

diff --git 
a/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/ClusterAccessor.java
 
b/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/ClusterAccessor.java
index cb56d009c..1e752f3fe 100644
--- 
a/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/ClusterAccessor.java
+++ 
b/helix-rest/src/main/java/org/apache/helix/rest/server/resources/helix/ClusterAccessor.java
@@ -60,6 +60,7 @@ import org.apache.helix.model.ClusterConfig;
 import org.apache.helix.model.ControllerHistory;
 import org.apache.helix.model.CustomizedStateConfig;
 import org.apache.helix.model.HelixConfigScope;
+import org.apache.helix.model.InstanceConfig;
 import org.apache.helix.model.LiveInstance;
 import org.apache.helix.model.MaintenanceSignal;
 import org.apache.helix.model.Message;
@@ -708,9 +709,11 @@ public class ClusterAccessor extends AbstractHelixResource 
{
     try {
       switch (command) {
         case update:
+          validateClusterConfigChange(clusterId, configAccessor, config, 
command);
           configAccessor.updateClusterConfig(clusterId, config);
           break;
         case delete: {
+          validateClusterConfigChange(clusterId, configAccessor, config, 
command);
           HelixConfigScope clusterScope =
               new 
HelixConfigScopeBuilder(HelixConfigScope.ConfigScopeProperty.CLUSTER)
                   .forCluster(clusterId).build();
@@ -1248,4 +1251,59 @@ public class ClusterAccessor extends 
AbstractHelixResource {
   private AclRegister getAclRegister() {
     return (AclRegister) 
_application.getProperties().get(ContextPropertyKeys.ACL_REGISTER.name());
   }
+
+  /**
+   * Validates the changes to the cluster configuration.
+   *
+   * Specifically checks changes related to topology settings. If topology 
settings are updated,
+   * ensures all instance configurations align with the new cluster 
configuration.
+   *
+   * @param clusterName Name of the cluster to validate.
+   * @param configAccessor Accessor to retrieve and update cluster 
configurations.
+   * @param newClusterConfig The new cluster configuration to validate against.
+   * @param command Type of command triggering this validation (e.g., update, 
delete).
+   */
+  private void validateClusterConfigChange(String clusterName, ConfigAccessor 
configAccessor,
+      ClusterConfig newClusterConfig, Command command) {
+    ClusterConfig oldConfig = configAccessor.getClusterConfig(clusterName);
+    ClusterConfig updatedConfig = configAccessor.getClusterConfig(clusterName);
+
+    if (command == Command.delete) {
+      // Since the topology related setting is only in the simple field, we 
don't need to validate
+      // other fields.
+      for (Map.Entry<String, String> entry : 
newClusterConfig.getRecord().getSimpleFields()
+          .entrySet()) {
+        updatedConfig.getRecord().getSimpleFields().remove(entry.getKey());
+      }
+    } else {
+      updatedConfig.getRecord().update(newClusterConfig.getRecord());
+    }
+
+    // Only validate the topology related settings if the topology aware is 
enabled.
+    if (updatedConfig.isTopologyAwareEnabled()) {
+      if (updatedConfig.getTopology() == null || 
updatedConfig.getFaultZoneType() == null) {
+        throw new IllegalArgumentException(
+            "Topology and fault zone type must be set when topology aware is 
enabled.");
+      }
+
+      boolean isTopologyAwareChanged =
+          !oldConfig.isTopologyAwareEnabled() && 
updatedConfig.isTopologyAwareEnabled();
+      boolean isTopologyPathChanged =
+          oldConfig.getTopology() == null || (oldConfig.getTopology() != null
+              && !oldConfig.getTopology().equals(updatedConfig.getTopology()));
+      boolean isFaultZoneTypeChanged =
+          oldConfig.getFaultZoneType() == null || 
(oldConfig.getFaultZoneType() != null
+              && 
!oldConfig.getFaultZoneType().equals(updatedConfig.getFaultZoneType()));
+
+      if (isTopologyAwareChanged || isTopologyPathChanged || 
isFaultZoneTypeChanged) {
+        HelixDataAccessor dataAccessor = getDataAccssor(clusterName);
+        PropertyKey.Builder keyBuilder = dataAccessor.keyBuilder();
+        List<InstanceConfig> instanceConfigs = 
dataAccessor.getChildValues(keyBuilder.instanceConfigs(), true);
+        for (InstanceConfig instanceConfig : instanceConfigs) {
+          instanceConfig.validateTopologySettingInInstanceConfig(updatedConfig,
+              instanceConfig.getInstanceName());
+        }
+      }
+    }
+  }
 }
diff --git 
a/helix-rest/src/test/java/org/apache/helix/rest/server/TestClusterAccessor.java
 
b/helix-rest/src/test/java/org/apache/helix/rest/server/TestClusterAccessor.java
index 4ae7b3dfe..619a78127 100644
--- 
a/helix-rest/src/test/java/org/apache/helix/rest/server/TestClusterAccessor.java
+++ 
b/helix-rest/src/test/java/org/apache/helix/rest/server/TestClusterAccessor.java
@@ -90,6 +90,72 @@ public class TestClusterAccessor extends AbstractTestClass {
   }
 
   @Test
+  public void testValidateClusterConfigChange() throws IOException {
+    System.out.println("Start test :" + TestHelper.getTestMethodName());
+    String cluster = "TestCluster_1";
+    ClusterConfig config = getClusterConfigFromRest(cluster);
+
+    // Enable the topology aware setting while the instance config does not 
have the DOMAIN info
+    {
+      ClusterConfig newConfig = new ClusterConfig(config.getRecord());
+      newConfig.setTopologyAwareEnabled(true);
+      _auditLogger.clearupLogs();
+      Entity entity = 
Entity.entity(OBJECT_MAPPER.writeValueAsString(newConfig.getRecord()),
+          MediaType.APPLICATION_JSON_TYPE);
+      post("clusters/" + cluster + "/configs", ImmutableMap.of("command", 
Command.update.name()),
+          entity, Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
+
+      validateAuditLogSize(1);
+      AuditLog auditLog = _auditLogger.getAuditLogs().get(0);
+      Assert.assertEquals(auditLog.getHttpMethod(), HTTPMethods.POST.name());
+      Assert.assertEquals(auditLog.getRequestPath(), "clusters/" + cluster + 
"/configs");
+      Assert.assertEquals(auditLog.getExceptions().size(), 1);
+    }
+
+    // Since the topology aware is not enabled, changing the cluster config 
should not fail even
+    // if the instance config does not have the DOMAIN info
+    {
+      ClusterConfig newConfig = new ClusterConfig(config.getRecord());
+      newConfig.setFaultZoneType("TestZoneId");
+      newConfig.setTopology("/TestZoneId/instance");
+      newConfig.setTopologyAwareEnabled(false);
+      updateClusterConfigFromRest(cluster, newConfig, Command.update);
+    }
+
+    // Update the topology path string to NULL. This request should go through 
since the
+    // topology aware is not enabled.
+    {
+      ClusterConfig newConfig = new ClusterConfig(config.getRecord());
+      newConfig.setTopology(null);
+      newConfig.setFaultZoneType(null);
+      newConfig.setTopologyAwareEnabled(false);
+      updateClusterConfigFromRest(cluster, newConfig, Command.update);
+    }
+
+    // Now update the config while keeping the topology path and fault zone 
unchanged (it's still NULL)
+    {
+      ClusterConfig newConfig = new ClusterConfig(config.getClusterName());
+      newConfig.setTopologyAwareEnabled(true);
+      _auditLogger.clearupLogs();
+      Entity entity = 
Entity.entity(OBJECT_MAPPER.writeValueAsString(newConfig.getRecord()),
+          MediaType.APPLICATION_JSON_TYPE);
+      post("clusters/" + cluster + "/configs", ImmutableMap.of("command", 
Command.update.name()),
+          entity, Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
+
+      validateAuditLogSize(1);
+      AuditLog auditLog = _auditLogger.getAuditLogs().get(0);
+      Assert.assertEquals(auditLog.getHttpMethod(), HTTPMethods.POST.name());
+      Assert.assertEquals(auditLog.getRequestPath(), "clusters/" + cluster + 
"/configs");
+      Assert.assertEquals(auditLog.getExceptions().size(), 1);
+    }
+
+    // Restore the cluster config
+    updateClusterConfigFromRest(cluster, config, Command.update);
+
+    System.out.println("End test :" + TestHelper.getTestMethodName());
+  }
+
+  @Test(dependsOnMethods = "testValidateClusterConfigChange")
   public void testGetClusters() throws IOException {
     System.out.println("Start test :" + TestHelper.getTestMethodName());
 

Reply via email to