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

JiaLiangC pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 2f794b18ad AMBARI-26614: Config and config-group changes are slow to 
propagate to agents on large clusters (#4135)
2f794b18ad is described below

commit 2f794b18ad493f260ad38fff2f5f4651b2cfb16f
Author: Yubi Lee <[email protected]>
AuthorDate: Wed Jun 10 11:13:01 2026 +0900

    AMBARI-26614: Config and config-group changes are slow to propagate to 
agents on large clusters (#4135)
    
    Reduce the work done on each config / config-group change and fix a config
    staleness issue uncovered while doing so.
    
    - Scope the agent-config push: when a config type changes, recompute/push 
only to
      hosts whose installed components depend on (or whose service owns) that 
type
      (ConfigHelper.getHostsAffectedByConfigTypes + 
AmbariManagementControllerImpl).
      Types not owned by any installed service (e.g. cluster-env) still fan out 
to all
      hosts. Host-set scoping only — each host's config payload content is 
unchanged.
    
    - Pre-resolve the changed cluster's desired configs on the request thread 
before the
      parallel per-host recompute. Resolving them lazily inside the 
ForkJoinPool worker
      (cachedClustersDesiredConfigs.computeIfAbsent(clusterId, id -> 
cl.getDesiredConfigs(false)))
      runs outside the request's transaction/persistence context, so it reads 
the
      pre-change committed snapshot and pushes the previous config value to 
agents — a
      deterministic off-by-one that leaves a host one change behind until an
      ambari-server/agent restart. Seeding the cache on the request thread 
(read-your-writes)
      keeps the parallel recompute while making the workers never call 
getDesiredConfigs.
    
    - Scope config-group changes to the group's member hosts instead of 
recomputing
      every cluster host (ConfigGroupResourceProvider + 
updateAgentConfigs(Cluster, List<Long>)),
      with hostId-based group lookup. Covered by 
ConfigGroupResourceProviderTest.
    
    - Avoid redundant work per change: cache each cluster's desired configs 
once per batch,
      drop unnecessary agent-payload sorting (ClusterConfigs) and orphaned 
unescape logic.
---
 .../server/agent/stomp/AgentConfigsHolder.java     |  62 ++++-
 .../server/agent/stomp/AgentHostDataHolder.java    |  22 ++
 .../server/agent/stomp/AlertDefinitionsHolder.java |   7 +
 .../server/agent/stomp/HostLevelParamsHolder.java  |   7 +
 .../server/agent/stomp/dto/ClusterConfigs.java     |   8 +
 .../controller/AmbariManagementControllerImpl.java |   9 +-
 .../internal/ConfigGroupResourceProvider.java      |  32 ++-
 .../org/apache/ambari/server/state/Cluster.java    |   8 +
 .../apache/ambari/server/state/ConfigHelper.java   | 265 ++++++++++++++-------
 .../ambari/server/state/cluster/ClusterImpl.java   |  17 ++
 .../ambari/server/state/cluster/ClustersImpl.java  |  31 ++-
 .../apache/ambari/server/state/host/HostImpl.java  |   2 +-
 .../topology/STOMPComponentsDeleteHandler.java     |   8 +-
 .../internal/ConfigGroupResourceProviderTest.java  | 261 ++++++++++++++++++++
 .../ambari/server/state/ConfigHelperTest.java      |  41 ++++
 15 files changed, 669 insertions(+), 111 deletions(-)

diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AgentConfigsHolder.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AgentConfigsHolder.java
index 22eebe909e..5c04557751 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AgentConfigsHolder.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AgentConfigsHolder.java
@@ -19,6 +19,9 @@ package org.apache.ambari.server.agent.stomp;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
 
 import org.apache.ambari.server.AmbariException;
@@ -27,6 +30,7 @@ import 
org.apache.ambari.server.events.publishers.AmbariEventPublisher;
 import org.apache.ambari.server.security.encryption.Encryptor;
 import org.apache.ambari.server.state.Clusters;
 import org.apache.ambari.server.state.ConfigHelper;
+import org.apache.ambari.server.state.DesiredConfig;
 import org.apache.ambari.server.state.Host;
 import org.apache.ambari.server.utils.ThreadPools;
 import org.apache.commons.collections4.CollectionUtils;
@@ -64,10 +68,21 @@ public class AgentConfigsHolder extends 
AgentHostDataHolder<AgentConfigsUpdateEv
     return configHelper.getHostActualConfigs(hostId);
   }
 
+  @Override
+  public AgentConfigsUpdateEvent getCurrentData(Long hostId,
+      Map<Long, Map<String, DesiredConfig>> cachedClustersDesiredConfigs) 
throws AmbariException {
+    return configHelper.getHostActualConfigs(hostId, 
cachedClustersDesiredConfigs);
+  }
+
   public AgentConfigsUpdateEvent getCurrentDataExcludeCluster(Long hostId, 
Long clusterId) throws AmbariException {
     return configHelper.getHostActualConfigsExcludeCluster(hostId, clusterId);
   }
 
+  public AgentConfigsUpdateEvent getCurrentDataExcludeCluster(Long hostId, 
Long clusterId,
+      Map<Long, Map<String, DesiredConfig>> cachedClustersDesiredConfigs) 
throws AmbariException {
+    return configHelper.getHostActualConfigsExcludeCluster(hostId, clusterId, 
cachedClustersDesiredConfigs);
+  }
+
   @Override
   protected AgentConfigsUpdateEvent handleUpdate(AgentConfigsUpdateEvent 
current, AgentConfigsUpdateEvent update) {
     return update;
@@ -84,9 +99,50 @@ public class AgentConfigsHolder extends 
AgentHostDataHolder<AgentConfigsUpdateEv
       }
     }
 
-    for (Long hostId : hostIds) {
-      AgentConfigsUpdateEvent agentConfigsUpdateEvent = 
configHelper.getHostActualConfigs(hostId);
-      updateData(agentConfigsUpdateEvent);
+    final List<Long> targetHostIds = hostIds;
+    // Resolve each host's full (all-cluster) config so a host belonging to 
more than one cluster does
+    // not lose the other clusters' configs when its cached event is replaced. 
The shared cache
+    // (concurrent for the parallel path) resolves each cluster's desired 
configs at most once.
+    Map<Long, Map<String, DesiredConfig>> cachedClustersDesiredConfigs = new 
ConcurrentHashMap<>();
+    // IMPORTANT - DO NOT MOVE THIS READ INTO THE PARALLEL BLOCK BELOW.
+    // Pre-resolve the changed cluster's desired configs here, on the calling 
(request) thread, while
+    // that thread's write transaction is still open. The per-host recompute 
below runs on ForkJoinPool
+    // workers, and ConfigHelper.getHostActualConfigsExcludeCluster resolves a 
cluster's desired configs
+    // lazily via:  cachedClustersDesiredConfigs.computeIfAbsent(clusterId, id 
-> cl.getDesiredConfigs(false))
+    // A pool worker is a DIFFERENT thread with no inherited persistence 
context/transaction (the
+    // EntityManager/transaction is bound to the writing thread), so that lazy 
read sees only the last
+    // COMMITTED snapshot - never this request's still-uncommitted change - 
and pushes the PREVIOUS
+    // config value to agents. Because .get() keeps this transaction waiting 
for the workers, the worker
+    // read is always pre-commit, producing a deterministic off-by-one: each 
change lands on agents one
+    // change late, and the host stays stale until an ambari-server/agent 
restart re-resolves it.
+    // Seeding the cache here (read-your-writes on the request thread => fresh 
value) makes computeIfAbsent
+    // a no-op for this cluster, so the workers never call getDesiredConfigs 
themselves.
+    cachedClustersDesiredConfigs.put(clusterId, 
clusters.get().getCluster(clusterId).getDesiredConfigs(false));
+    // Process every host so successful updates are still delivered even if 
some fail,
+    // but collect failures and rethrow afterwards so the operator is notified.
+    Map<Long, AmbariException> failures = new ConcurrentHashMap<>();
+    try {
+      // run on the shared default ForkJoinPool so per-host recomputation is 
parallelized
+      // without leaking a pool; .get() waits for all hosts to be processed 
before returning.
+      threadPools.getDefaultForkJoinPool().submit(() ->
+        targetHostIds.parallelStream().forEach(hostId -> {
+          try {
+            updateData(configHelper.getHostActualConfigs(hostId, 
cachedClustersDesiredConfigs));
+          } catch (AmbariException e) {
+            LOG.error("Agent configs update was failed for host {}", hostId, 
e);
+            failures.put(hostId, e);
+          }
+        })
+      ).get();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      throw new AmbariException("Agent configs update was interrupted", e);
+    } catch (ExecutionException e) {
+      throw new AmbariException("Agent configs update was failed", e);
+    }
+    if (!failures.isEmpty()) {
+      throw new AmbariException(String.format("Agent configs update failed for 
%d of %d host(s): %s",
+          failures.size(), targetHostIds.size(), failures.keySet()), 
failures.values().iterator().next());
     }
   }
 
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AgentHostDataHolder.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AgentHostDataHolder.java
index 003a2b7945..6ed4b6d959 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AgentHostDataHolder.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AgentHostDataHolder.java
@@ -30,6 +30,7 @@ import org.apache.ambari.server.agent.stomp.dto.Hashable;
 import org.apache.ambari.server.events.STOMPEvent;
 import org.apache.ambari.server.events.STOMPHostEvent;
 import org.apache.ambari.server.events.publishers.STOMPUpdatePublisher;
+import org.apache.ambari.server.state.DesiredConfig;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -46,6 +47,7 @@ public abstract class AgentHostDataHolder<T extends 
STOMPHostEvent & Hashable> e
   private final ConcurrentHashMap<Long, T> data = new ConcurrentHashMap<>();
 
   protected abstract T getCurrentData(Long hostId) throws AmbariException;
+  protected abstract T getCurrentData(Long hostId, Map<Long, Map<String, 
DesiredConfig>> cachedClustersDesiredConfigs) throws AmbariException;
   protected abstract T handleUpdate(T current, T update) throws 
AmbariException;
 
   public T getUpdateIfChanged(String agentHash, Long hostId) throws 
AmbariException {
@@ -71,6 +73,26 @@ public abstract class AgentHostDataHolder<T extends 
STOMPHostEvent & Hashable> e
     return hostData;
   }
 
+  public T initializeDataIfNeeded(Long hostId, boolean regenerateHash,
+      Map<Long, Map<String, DesiredConfig>> cachedClustersDesiredConfigs) 
throws AmbariRuntimeException {
+    return data.computeIfAbsent(hostId, id -> initializeData(hostId, 
regenerateHash, cachedClustersDesiredConfigs));
+  }
+
+  private T initializeData(Long hostId, boolean regenerateHash,
+      Map<Long, Map<String, DesiredConfig>> cachedClustersDesiredConfigs) {
+    T hostData;
+    try {
+      hostData = getCurrentData(hostId, cachedClustersDesiredConfigs);
+    } catch (AmbariException e) {
+      LOG.error("Error during retrieving initial value for host: {} and class 
{}", hostId, getClass().getName(), e);
+      throw new AmbariRuntimeException("Error during retrieving initial value 
for host: " + hostId + " and class: " + getClass().getName(), e);
+    }
+    if (regenerateHash) {
+      regenerateDataIdentifiers(hostData);
+    }
+    return hostData;
+  }
+
   /**
    * Apply an incremental update to the data (host-specific), and publish the
    * event to listeners.
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AlertDefinitionsHolder.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AlertDefinitionsHolder.java
index 4e1edb1bfa..10fe3f3034 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AlertDefinitionsHolder.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/AlertDefinitionsHolder.java
@@ -41,6 +41,7 @@ import 
org.apache.ambari.server.events.publishers.AmbariEventPublisher;
 import org.apache.ambari.server.orm.dao.AlertDefinitionDAO;
 import org.apache.ambari.server.orm.entities.AlertDefinitionEntity;
 import org.apache.ambari.server.state.Clusters;
+import org.apache.ambari.server.state.DesiredConfig;
 import org.apache.ambari.server.state.alert.AlertDefinition;
 import org.apache.ambari.server.state.alert.AlertDefinitionFactory;
 import org.apache.ambari.server.state.alert.AlertDefinitionHash;
@@ -77,6 +78,12 @@ public class AlertDefinitionsHolder extends 
AgentHostDataHolder<AlertDefinitions
     eventPublisher.register(this);
   }
 
+  @Override
+  protected AlertDefinitionsAgentUpdateEvent getCurrentData(Long hostId,
+      Map<Long, Map<String, DesiredConfig>> cachedClustersDesiredConfigs) 
throws AmbariException {
+    return getCurrentData(hostId);
+  }
+
   @Override
   protected AlertDefinitionsAgentUpdateEvent getCurrentData(Long hostId) 
throws AmbariException {
     Map<Long, AlertCluster> result = new TreeMap<>();
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/HostLevelParamsHolder.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/HostLevelParamsHolder.java
index 5c0efd5b42..7674233830 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/HostLevelParamsHolder.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/HostLevelParamsHolder.java
@@ -34,6 +34,7 @@ import 
org.apache.ambari.server.events.publishers.AmbariEventPublisher;
 import org.apache.ambari.server.state.BlueprintProvisioningState;
 import org.apache.ambari.server.state.Cluster;
 import org.apache.ambari.server.state.Clusters;
+import org.apache.ambari.server.state.DesiredConfig;
 import org.apache.ambari.server.state.Host;
 import org.apache.ambari.server.state.ServiceComponentHost;
 import org.apache.commons.collections4.MapUtils;
@@ -60,6 +61,12 @@ public class HostLevelParamsHolder extends 
AgentHostDataHolder<HostLevelParamsUp
     ambariEventPublisher.register(this);
   }
 
+  @Override
+  public HostLevelParamsUpdateEvent getCurrentData(Long hostId,
+      Map<Long, Map<String, DesiredConfig>> cachedClustersDesiredConfigs) 
throws AmbariException {
+    return getCurrentData(hostId);
+  }
+
   @Override
   public HostLevelParamsUpdateEvent getCurrentData(Long hostId) throws 
AmbariException {
     return getCurrentDataExcludeCluster(hostId, null);
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/dto/ClusterConfigs.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/dto/ClusterConfigs.java
index 296fb2f0a3..c252060c2a 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/dto/ClusterConfigs.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/agent/stomp/dto/ClusterConfigs.java
@@ -22,6 +22,14 @@ import java.util.SortedMap;
 
 import com.fasterxml.jackson.annotation.JsonInclude;
 
+/**
+ * Configs sent to an agent for a single cluster.
+ *
+ * The configurations are intentionally kept in {@link SortedMap}s so the JSON 
serialization
+ * has a stable, deterministic key order. The per-host config hash is computed 
from this JSON
+ * (see AgentDataHolder#getHash); an unstable key order would make the hash 
change for unchanged
+ * configs and trigger spurious config updates (and stale-config / restart 
prompts) on every agent.
+ */
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public class ClusterConfigs {
   private SortedMap<String, SortedMap<String, String>> configurations;
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariManagementControllerImpl.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariManagementControllerImpl.java
index 90ebcd446b..b859afc8bb 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariManagementControllerImpl.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariManagementControllerImpl.java
@@ -1852,6 +1852,9 @@ public class AmbariManagementControllerImpl implements 
AmbariManagementControlle
       new LinkedList<>();
     ServiceConfigVersionResponse serviceConfigVersionResponse = null;
     boolean nonServiceConfigsChanged = false;
+    // Config types actually changed in this request; used to scope the 
agent-config push to the
+    // hosts whose components depend on them (empty => fall back to updating 
all hosts).
+    Set<String> changedConfigTypes = new HashSet<>();
 
     if (desiredConfigs != null && request.getServiceConfigVersionRequest() != 
null) {
       String msg = "Unable to set desired configs and rollback at same time, 
request = " + request;
@@ -1990,6 +1993,7 @@ public class AmbariManagementControllerImpl implements 
AmbariManagementControlle
           for (Config config : configs) {
             Config existingConfig = 
cluster.getDesiredConfigByType(config.getType());
             existingConfigTypeToConfig.put(config.getType(), existingConfig);
+            changedConfigTypes.add(config.getType());
           }
 
           String authName = getAuthName();
@@ -2165,7 +2169,10 @@ public class AmbariManagementControllerImpl implements 
AmbariManagementControlle
       }
     }
     if (fireAgentUpdates && (serviceConfigVersionResponse != null || 
nonServiceConfigsChanged)) {
-      
configHelper.updateAgentConfigs(Collections.singleton(cluster.getClusterName()));
+      // Scope the agent-config push to hosts whose components depend on the 
changed config types.
+      // When changedConfigTypes is empty (e.g. service-config-version revert 
path), this falls back
+      // to updating all cluster hosts.
+      
configHelper.updateAgentConfigs(Collections.singleton(cluster.getClusterName()),
 changedConfigTypes);
     }
 
     if (requestStageContainer != null) {
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ConfigGroupResourceProvider.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ConfigGroupResourceProvider.java
index 8548e45214..c843c2e1bb 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ConfigGroupResourceProvider.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/ConfigGroupResourceProvider.java
@@ -552,7 +552,6 @@ public class ConfigGroupResourceProvider extends
     ConfigGroupFactory configGroupFactory = getManagementController()
       .getConfigGroupFactory();
 
-    Set<String> updatedClusters = new HashSet<>();
     for (ConfigGroupRequest request : requests) {
 
       Cluster cluster;
@@ -640,11 +639,14 @@ public class ConfigGroupResourceProvider extends
         configGroup.getTag(), configGroup.getDescription(), null, null);
 
       configGroupResponses.add(response);
-      updatedClusters.add(cluster.getClusterName());
+      // a new config group only affects its member hosts; skip when it has 
none, otherwise an
+      // empty host list would fall back to updating every host in the cluster 
(see
+      // AgentConfigsHolder#updateData).
+      if (!hosts.isEmpty()) {
+        m_configHelper.get().updateAgentConfigs(cluster, new 
ArrayList<>(hosts.keySet()));
+      }
     }
 
-    m_configHelper.get().updateAgentConfigs(updatedClusters);
-
     return configGroupResponses;
   }
 
@@ -656,10 +658,10 @@ public class ConfigGroupResourceProvider extends
 
     Clusters clusters = getManagementController().getClusters();
 
-    Set<String> updatedClusters = new HashSet<>();
     for (ConfigGroupRequest request : requests) {
 
       Cluster cluster;
+      Set<Long> targetHostIds = new HashSet<>();
       try {
         cluster = clusters.getCluster(request.getClusterName());
       } catch (ClusterNotFoundException e) {
@@ -705,7 +707,12 @@ public class ConfigGroupResourceProvider extends
         serviceName = requestServiceName;
       }
 
-      int numHosts = (null != configGroup.getHosts()) ? 
configGroup.getHosts().size() : 0;
+      int numHosts = 0;
+      Map<Long, Host> prevHosts = configGroup.getHosts();
+      if (null != prevHosts && !prevHosts.isEmpty()) {
+        numHosts = prevHosts.size();
+        targetHostIds.addAll(prevHosts.keySet());
+      }
       configLogger.info("(configchange) Updating configuration group host 
membership or config value. cluster: '{}', changed by: '{}', " +
               "service_name: '{}', config group: '{}', tag: '{}', num hosts in 
config group: '{}', note: '{}'",
           cluster.getClusterName(), getManagementController().getAuthName(),
@@ -734,7 +741,9 @@ public class ConfigGroupResourceProvider extends
           if (hostEntity == null) {
             throw new HostNotFoundException(hostname);
           }
-          hosts.put(hostEntity.getHostId(), host);
+          Long hostId = hostEntity.getHostId();
+          hosts.put(hostId, host);
+          targetHostIds.add(hostId);
         }
       }
 
@@ -765,14 +774,17 @@ public class ConfigGroupResourceProvider extends
         versionTags.add(tagsMap);
         configGroupResponse.setVersionTags(versionTags);
         getManagementController().saveConfigGroupUpdate(request, 
configGroupResponse);
-        updatedClusters.add(cluster.getClusterName());
       } else {
         LOG.warn("Could not determine service name for config group {}, 
service config version not created",
             configGroup.getId());
       }
+      // only the affected hosts (previous + new members) need updating; skip 
when there are none,
+      // otherwise an empty host list would fall back to updating every host 
in the cluster (see
+      // AgentConfigsHolder#updateData).
+      if (!targetHostIds.isEmpty()) {
+        m_configHelper.get().updateAgentConfigs(cluster, new 
ArrayList<>(targetHostIds));
+      }
     }
-
-    m_configHelper.get().updateAgentConfigs(updatedClusters);
   }
 
   @SuppressWarnings("unchecked")
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/state/Cluster.java 
b/ambari-server/src/main/java/org/apache/ambari/server/state/Cluster.java
index 799e1dabb7..573570ad2c 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/Cluster.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/Cluster.java
@@ -529,6 +529,14 @@ public interface Cluster {
   Map<Long, ConfigGroup> getConfigGroupsByHostname(String hostname)
       throws AmbariException;
 
+  /**
+   * Find all config groups associated with the given hostId
+   * @param hostId
+   * @return Map of config group id to config group
+   */
+  Map<Long, ConfigGroup> getConfigGroupsByHostId(Long hostId)
+      throws AmbariException;
+
   /**
    * Find config group by config group id
    * @param configId id of config group to return
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/state/ConfigHelper.java 
b/ambari-server/src/main/java/org/apache/ambari/server/state/ConfigHelper.java
index 71efb4edb6..5bf31a83de 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/state/ConfigHelper.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/state/ConfigHelper.java
@@ -62,7 +62,6 @@ import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.collections4.MapUtils;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang.math.NumberUtils;
-import org.apache.commons.lang3.StringEscapeUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -1526,7 +1525,7 @@ public class ConfigHelper {
         HostConfig hc = actual.get(type);
         Map<String, String> actualTags = buildTags(hc);
 
-        if (!isTagChanged(tags, actualTags, 
hasGroupSpecificConfigsForType(cluster, sch.getHostName(), type))) {
+        if (!isTagChanged(tags, actualTags, 
hasGroupSpecificConfigsForType(cluster, sch.getHost().getHostId(), type))) {
           staleEntry = false;
         } else {
           staleEntry = (serviceInfo.hasConfigDependency(type) || 
componentInfo.hasConfigType(type));
@@ -1576,73 +1575,175 @@ public class ConfigHelper {
       clustersInUse.add(cluster);
     }
 
-    // get all current and previous host configs
-    Map<Long, AgentConfigsUpdateEvent> currentConfigEvents = new HashMap<>();
-    Map<Long, AgentConfigsUpdateEvent> previousConfigEvents = new HashMap<>();
     for (Cluster cluster : clustersInUse) {
+      Set<Long> hostIds = new HashSet<>();
       for (Host host : cluster.getHosts()) {
-        Long hostId = host.getHostId();
-        if (!currentConfigEvents.containsKey(hostId)) {
-          currentConfigEvents.put(host.getHostId(), 
m_agentConfigsHolder.get().getCurrentData(hostId));
+        hostIds.add(host.getHostId());
+      }
+      updateAgentConfigs(cluster, new ArrayList<>(hostIds));
+    }
+  }
+
+  /**
+   * Scoped variant of {@link #updateAgentConfigs(Set)}: only hosts running a 
component that depends
+   * on (or whose owning service defines) one of the changed config types are 
recomputed and pushed.
+   * Global/unowned types (e.g. cluster-env) fan out to all hosts. When the 
changed types are unknown
+   * (empty) this falls back to the all-hosts behavior of {@link 
#updateAgentConfigs(Set)}.
+   *
+   * @param updatedClusters names of clusters with changed configs
+   * @param changedConfigTypes the config types that actually changed (may be 
empty/unknown)
+   * @throws AmbariException
+   */
+  public void updateAgentConfigs(Set<String> updatedClusters, Set<String> 
changedConfigTypes) throws AmbariException {
+    if (changedConfigTypes == null || changedConfigTypes.isEmpty()) {
+      // Unknown which types changed: preserve the original all-hosts behavior.
+      updateAgentConfigs(updatedClusters);
+      return;
+    }
+    for (String clusterName : updatedClusters) {
+      Cluster cluster = clusters.getCluster(clusterName);
+      Set<Long> hostIds = getHostsAffectedByConfigTypes(cluster, 
changedConfigTypes);
+      if (!hostIds.isEmpty()) {
+        updateAgentConfigs(cluster, new ArrayList<>(hostIds));
+      }
+    }
+  }
+
+  /**
+   * Returns the ids of hosts that run at least one component whose service or 
component declares a
+   * dependency on, or whose service owns, any of the given changed config 
types. If any changed type
+   * is not owned by a service (e.g. a global type such as cluster-env), every 
cluster host is
+   * returned. The resulting set is a strict superset of the hosts the 
stale-config logic would mark
+   * for the same types (see {@link #calculateIsStaleConfigs}), so scoping 
config updates to it never
+   * skips a host that should be updated.
+   *
+   * @param cluster the cluster
+   * @param changedConfigTypes the changed config types
+   * @return ids of the affected hosts (empty when an owned type has no 
installed components)
+   * @throws AmbariException
+   */
+  public Set<Long> getHostsAffectedByConfigTypes(Cluster cluster, Set<String> 
changedConfigTypes) throws AmbariException {
+    Set<Long> affectedHostIds = new HashSet<>();
+    if (changedConfigTypes == null || changedConfigTypes.isEmpty()) {
+      return affectedHostIds;
+    }
+
+    // A type not owned by any service is treated as global (e.g. 
cluster-env): it affects every host.
+    for (String configType : changedConfigTypes) {
+      if (cluster.getServiceByConfigType(configType) == null) {
+        for (Host host : cluster.getHosts()) {
+          affectedHostIds.add(host.getHostId());
+        }
+        return affectedHostIds;
+      }
+    }
+
+    // Memoize ServiceInfo lookups within this call (avoids O(hosts x 
components) metainfo lookups).
+    Map<String, ServiceInfo> serviceInfoCache = new HashMap<>();
+
+    for (Host host : cluster.getHosts()) {
+      for (ServiceComponentHost sch : 
cluster.getServiceComponentHosts(host.getHostName())) {
+        StackId stackId = sch.getServiceComponent().getDesiredStackId();
+        String serviceKey = stackId.getStackName() + "/" + 
stackId.getStackVersion() + "/" + sch.getServiceName();
+        ServiceInfo serviceInfo = serviceInfoCache.get(serviceKey);
+        if (serviceInfo == null) {
+          serviceInfo = ambariMetaInfo.getService(stackId.getStackName(), 
stackId.getStackVersion(), sch.getServiceName());
+          serviceInfoCache.put(serviceKey, serviceInfo);
+        }
+        ComponentInfo componentInfo = 
serviceInfo.getComponentByName(sch.getServiceComponentName());
+
+        for (String configType : changedConfigTypes) {
+          // Same relevance the stale-config path uses (hasConfigDependency || 
hasConfigType), plus
+          // service ownership (hasConfigType on the service) to be safe 
against incomplete stack
+          // <configuration-dependencies> declarations.
+          if (serviceInfo.hasConfigDependency(configType)
+              || serviceInfo.hasConfigType(configType)
+              || (componentInfo != null && 
componentInfo.hasConfigType(configType))) {
+            affectedHostIds.add(host.getHostId());
+            break;
+          }
         }
-        if (!previousConfigEvents.containsKey(host.getHostId())) {
-          previousConfigEvents.put(host.getHostId(),
-              m_agentConfigsHolder.get().initializeDataIfNeeded(hostId, true));
+        if (affectedHostIds.contains(host.getHostId())) {
+          break;
         }
       }
     }
+    return affectedHostIds;
+  }
 
-    for (Cluster cluster : clustersInUse) {
-      Map<Long, Map<String, Collection<String>>> changedConfigs = new 
HashMap<>();
-      for (Host host : cluster.getHosts()) {
-        AgentConfigsUpdateEvent currentConfigData = 
currentConfigEvents.get(host.getHostId());
-        AgentConfigsUpdateEvent previousConfigsData = 
previousConfigEvents.get(host.getHostId());
-
-        SortedMap<String, SortedMap<String, String>> currentConfigs =
-            
currentConfigData.getClustersConfigs().get(Long.toString(cluster.getClusterId())).getConfigurations();
-        SortedMap<String, SortedMap<String, String>> previousConfigs =
-            
previousConfigsData.getClustersConfigs().get(Long.toString(cluster.getClusterId())).getConfigurations();
-
-        Map<String, Collection<String>> changedConfigsHost = new HashMap<>();
-        for (String currentConfigType : currentConfigs.keySet()) {
-          if (previousConfigs.containsKey(currentConfigType)) {
-            Set<String> changedKeys = new HashSet<>();
-            Map<String, String> currentTypedConfigs = 
currentConfigs.get(currentConfigType);
-            Map<String, String> previousTypedConfigs = 
previousConfigs.get(currentConfigType);
-
-            for (String currentKey : currentTypedConfigs.keySet()) {
-              if (!previousTypedConfigs.containsKey(currentKey)
-                  || 
!currentTypedConfigs.get(currentKey).equals(previousTypedConfigs.get(currentKey)))
 {
-                changedKeys.add(currentKey);
-              }
-            }
-            for (String previousKey : previousTypedConfigs.keySet()) {
-              if (!currentTypedConfigs.containsKey(previousKey)) {
-                changedKeys.add(previousKey);
-              }
-            }
+  /**
+   * Checks populated services for staled configs and updates agent configs 
for the given hosts only.
+   * Method retrieves actual agent configs and compares them with just 
generated to identify stale configs.
+   * Then config updates are sent to agents.
+   * @param cluster cluster with changed configs
+   * @param hostIds ids of hosts with changed configs
+   * @throws AmbariException
+   */
+  public void updateAgentConfigs(Cluster cluster, List<Long> hostIds) throws 
AmbariException {
+
+    // get all current and previous host configs
+    Map<Long, AgentConfigsUpdateEvent> currentConfigEvents = new HashMap<>();
+    Map<Long, AgentConfigsUpdateEvent> previousConfigEvents = new HashMap<>();
+
+    Map<Long, Map<String, DesiredConfig>> cachedClustersDesiredConfigs = new 
HashMap<>();
+    for (Long hostId : hostIds) {
+      if (!currentConfigEvents.containsKey(hostId)) {
+        currentConfigEvents.put(hostId, 
m_agentConfigsHolder.get().getCurrentData(hostId, 
cachedClustersDesiredConfigs));
+      }
+      if (!previousConfigEvents.containsKey(hostId)) {
+        previousConfigEvents.put(hostId, 
m_agentConfigsHolder.get().initializeDataIfNeeded(hostId, true));
+      }
+    }
+
+    Map<Long, Map<String, Collection<String>>> changedConfigs = new 
HashMap<>();
+    for (Long hostId : hostIds) {
+      AgentConfigsUpdateEvent currentConfigData = 
currentConfigEvents.get(hostId);
+      AgentConfigsUpdateEvent previousConfigsData = 
previousConfigEvents.get(hostId);
+
+      SortedMap<String, SortedMap<String, String>> currentConfigs =
+          
currentConfigData.getClustersConfigs().get(Long.toString(cluster.getClusterId())).getConfigurations();
+      SortedMap<String, SortedMap<String, String>> previousConfigs =
+          
previousConfigsData.getClustersConfigs().get(Long.toString(cluster.getClusterId())).getConfigurations();
 
-            if (!changedKeys.isEmpty()) {
-              changedConfigsHost.put(currentConfigType, changedKeys);
+      Map<String, Collection<String>> changedConfigsHost = new HashMap<>();
+      for (String currentConfigType : currentConfigs.keySet()) {
+        if (previousConfigs.containsKey(currentConfigType)) {
+          Set<String> changedKeys = new HashSet<>();
+          Map<String, String> currentTypedConfigs = 
currentConfigs.get(currentConfigType);
+          Map<String, String> previousTypedConfigs = 
previousConfigs.get(currentConfigType);
+
+          for (String currentKey : currentTypedConfigs.keySet()) {
+            if (!previousTypedConfigs.containsKey(currentKey)
+                || 
!currentTypedConfigs.get(currentKey).equals(previousTypedConfigs.get(currentKey)))
 {
+              changedKeys.add(currentKey);
             }
-          } else {
-            changedConfigsHost.put(currentConfigType, 
currentConfigs.get(currentConfigType).keySet());
           }
-        }
-        for (String previousConfigType : previousConfigs.keySet()) {
-          if (!currentConfigs.containsKey(previousConfigType)) {
-            changedConfigsHost.put(previousConfigType, 
previousConfigs.get(previousConfigType).keySet());
+          for (String previousKey : previousTypedConfigs.keySet()) {
+            if (!currentTypedConfigs.containsKey(previousKey)) {
+              changedKeys.add(previousKey);
+            }
+          }
+
+          if (!changedKeys.isEmpty()) {
+            changedConfigsHost.put(currentConfigType, changedKeys);
           }
+        } else {
+          changedConfigsHost.put(currentConfigType, 
currentConfigs.get(currentConfigType).keySet());
         }
-        changedConfigs.put(host.getHostId(), changedConfigsHost);
       }
-      for (String serviceName : cluster.getServices().keySet()) {
-        checkStaleConfigsStatusOnConfigsUpdate(cluster.getClusterId(), 
serviceName, changedConfigs);
+      for (String previousConfigType : previousConfigs.keySet()) {
+        if (!currentConfigs.containsKey(previousConfigType)) {
+          changedConfigsHost.put(previousConfigType, 
previousConfigs.get(previousConfigType).keySet());
+        }
       }
-
-      
m_metadataHolder.get().updateData(m_ambariManagementController.get().getClusterMetadataOnConfigsUpdate(cluster));
-      m_agentConfigsHolder.get().updateData(cluster.getClusterId(), null);
+      changedConfigs.put(hostId, changedConfigsHost);
     }
+    for (String serviceName : cluster.getServices().keySet()) {
+      checkStaleConfigsStatusOnConfigsUpdate(cluster.getClusterId(), 
serviceName, changedConfigs);
+    }
+
+    
m_metadataHolder.get().updateData(m_ambariManagementController.get().getClusterMetadataOnConfigsUpdate(cluster));
+    m_agentConfigsHolder.get().updateData(cluster.getClusterId(), hostIds);
   }
 
   /**
@@ -1910,13 +2011,13 @@ public class ConfigHelper {
    * Determines if the hostname has group specific configs for the type 
specified
    *
    * @param cluster
-   * @param hostname of the host to look for
+   * @param hostId of the host to look for
    * @param type     the type to look for (e.g. flume-conf)
    * @return <code>true</code> if the hostname has group specific 
configuration for the type
    */
-  private boolean hasGroupSpecificConfigsForType(Cluster cluster, String 
hostname, String type) {
+  private boolean hasGroupSpecificConfigsForType(Cluster cluster, Long hostId, 
String type) {
     try {
-      Map<Long, ConfigGroup> configGroups = 
cluster.getConfigGroupsByHostname(hostname);
+      Map<Long, ConfigGroup> configGroups = 
cluster.getConfigGroupsByHostId(hostId);
       if (configGroups != null && !configGroups.isEmpty()) {
         for (ConfigGroup configGroup : configGroups.values()) {
           Config config = configGroup.getConfigurations().get(type);
@@ -2098,7 +2199,17 @@ public class ConfigHelper {
     return getHostActualConfigsExcludeCluster(hostId, null);
   }
 
+  public AgentConfigsUpdateEvent getHostActualConfigs(Long hostId,
+      Map<Long, Map<String, DesiredConfig>> cachedClustersDesiredConfigs) 
throws AmbariException {
+    return getHostActualConfigsExcludeCluster(hostId, null, 
cachedClustersDesiredConfigs);
+  }
+
   public AgentConfigsUpdateEvent getHostActualConfigsExcludeCluster(Long 
hostId, Long clusterId) throws AmbariException {
+    return getHostActualConfigsExcludeCluster(hostId, clusterId, new 
HashMap<>());
+  }
+
+  public AgentConfigsUpdateEvent getHostActualConfigsExcludeCluster(Long 
hostId, Long clusterId,
+      Map<Long, Map<String, DesiredConfig>> cachedClustersDesiredConfigs) 
throws AmbariException {
     TreeMap<String, ClusterConfigs> clustersConfigs = new TreeMap<>();
 
     Host host = clusters.getHostById(hostId);
@@ -2108,7 +2219,10 @@ public class ConfigHelper {
       }
       Map<String, Map<String, String>> configurations = new HashMap<>();
       Map<String, Map<String, Map<String, String>>> configurationAttributes = 
new HashMap<>();
-      Map<String, DesiredConfig> clusterDesiredConfigs = 
cl.getDesiredConfigs(false);
+      // computeIfAbsent so the desired configs are resolved at most once per 
cluster, even when this
+      // cache is shared across threads (the parallel host-init path uses a 
ConcurrentHashMap).
+      Map<String, DesiredConfig> clusterDesiredConfigs =
+          cachedClustersDesiredConfigs.computeIfAbsent(cl.getClusterId(), id 
-> cl.getDesiredConfigs(false));
       Map<String, Map<String, String>> configTags = 
getEffectiveDesiredTags(cl, host.getHostName(), clusterDesiredConfigs);
 
       // Logging below creating too much spam and slowing down operations
@@ -2119,49 +2233,26 @@ public class ConfigHelper {
       }
 
       getAndMergeHostConfigs(configurations, configTags, cl);
-      configurations = unescapeConfigNames(configurations);
       getAndMergeHostConfigAttributes(configurationAttributes, configTags, cl);
-      configurationAttributes = 
unescapeConfigAttributeNames(configurationAttributes);
 
-      SortedMap<String, SortedMap<String, String>> configurationsTreeMap = 
sortConfigutations(configurations);
-      SortedMap<String, SortedMap<String, SortedMap<String, String>>> 
configurationAttributesTreeMap =
-          sortConfigurationAttributes(configurationAttributes);
+      // Keep the payload sorted so the per-host config hash (computed from 
the JSON serialization)
+      // is stable for unchanged configs; see ClusterConfigs.
       clustersConfigs.put(Long.toString(cl.getClusterId()),
-          new ClusterConfigs(configurationsTreeMap, 
configurationAttributesTreeMap));
+          new ClusterConfigs(sortConfigurations(configurations), 
sortConfigurationAttributes(configurationAttributes)));
     }
 
     return new AgentConfigsUpdateEvent(hostId, clustersConfigs);
   }
 
-  private Map<String, Map<String, String>> unescapeConfigNames(Map<String, 
Map<String, String>> configurations) {
-    Map<String, Map<String, String>> unescapedConfigs = new HashMap<>();
-    for (Entry<String, Map<String, String>> configTypeEntry : 
configurations.entrySet()) {
-      Map<String, String> unescapedTypeConfigs = new HashMap<>();
-      for (Entry<String, String> config : 
configTypeEntry.getValue().entrySet()) {
-        
unescapedTypeConfigs.put(StringEscapeUtils.unescapeJava(config.getKey()), 
config.getValue());
-      }
-      unescapedConfigs.put(configTypeEntry.getKey(), unescapedTypeConfigs);
-    }
-
-    return unescapedConfigs;
-  }
-
-  private Map<String, Map<String, Map<String, String>>> 
unescapeConfigAttributeNames(
-      Map<String, Map<String, Map<String, String>>> configurationAttributes) {
-    Map<String, Map<String, Map<String, String>>> unescapedConfigAttributes = 
new HashMap<>();
-
-    configurationAttributes.forEach((key, value) ->  
unescapedConfigAttributes.put(key, unescapeConfigNames(value)));
-    return unescapedConfigAttributes;
-  }
-
-  public SortedMap<String, SortedMap<String, String>> 
sortConfigutations(Map<String, Map<String, String>> configurations) {
+  private SortedMap<String, SortedMap<String, String>> 
sortConfigurations(Map<String, Map<String, String>> configurations) {
     SortedMap<String, SortedMap<String, String>> configurationsTreeMap = new 
TreeMap<>();
     configurations.forEach((k, v) -> configurationsTreeMap.put(k, new 
TreeMap<>(v)));
     return configurationsTreeMap;
   }
 
-  public SortedMap<String, SortedMap<String, SortedMap<String, String>>> 
sortConfigurationAttributes(
+  private SortedMap<String, SortedMap<String, SortedMap<String, String>>> 
sortConfigurationAttributes(
       Map<String, Map<String, Map<String, String>>> configurationAttributes) {
+
     SortedMap<String, SortedMap<String, SortedMap<String, String>>> 
configurationAttributesTreeMap = new TreeMap<>();
     configurationAttributes.forEach((k, v) -> {
       SortedMap<String, SortedMap<String, String>> c = new TreeMap<>();
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/state/cluster/ClusterImpl.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/state/cluster/ClusterImpl.java
index c4eb82176a..93bc00402c 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/state/cluster/ClusterImpl.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/state/cluster/ClusterImpl.java
@@ -535,6 +535,23 @@ public class ClusterImpl implements Cluster {
     return configGroups;
   }
 
+  @Override
+  public Map<Long, ConfigGroup> getConfigGroupsByHostId(Long hostId) {
+    Map<Long, ConfigGroup> configGroups = new HashMap<>();
+
+    for (Entry<Long, ConfigGroup> groupEntry : clusterConfigGroups.entrySet()) 
{
+      Long id = groupEntry.getKey();
+      ConfigGroup group = groupEntry.getValue();
+      for (Host host : group.getHosts().values()) {
+        if (Objects.equals(hostId, host.getHostId())) {
+          configGroups.put(id, group);
+          break;
+        }
+      }
+    }
+    return configGroups;
+  }
+
   @Override
   public ConfigGroup getConfigGroupsById(Long configId) {
     return clusterConfigGroups.get(configId);
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/state/cluster/ClustersImpl.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/state/cluster/ClustersImpl.java
index 10497e100a..df381eea8b 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/state/cluster/ClustersImpl.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/state/cluster/ClustersImpl.java
@@ -30,6 +30,7 @@ import java.util.Set;
 import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutionException;
 
 import jakarta.persistence.RollbackException;
 
@@ -76,6 +77,7 @@ import 
org.apache.ambari.server.security.authorization.ResourceType;
 import org.apache.ambari.server.state.AgentVersion;
 import org.apache.ambari.server.state.Cluster;
 import org.apache.ambari.server.state.Clusters;
+import org.apache.ambari.server.state.DesiredConfig;
 import org.apache.ambari.server.state.Host;
 import org.apache.ambari.server.state.HostHealthStatus;
 import org.apache.ambari.server.state.HostHealthStatus.HealthStatus;
@@ -86,6 +88,7 @@ import org.apache.ambari.server.state.configgroup.ConfigGroup;
 import org.apache.ambari.server.state.host.HostFactory;
 import org.apache.ambari.server.topology.TopologyManager;
 import org.apache.ambari.server.utils.RetryHelper;
+import org.apache.ambari.server.utils.ThreadPools;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.security.core.GrantedAuthority;
@@ -157,6 +160,9 @@ public class ClustersImpl implements Clusters {
   @Inject
   private Provider<AgentConfigsHolder> m_agentConfigsHolder;
 
+  @Inject
+  private ThreadPools threadPools;
+
   @Inject
   private Provider<MetadataHolder> m_metadataHolder;
 
@@ -318,13 +324,24 @@ public class ClustersImpl implements Clusters {
     }
     hostClustersMap = hostClustersMapTemp;
     clusterHostsMap1 = clusterHostsMap1Temp;
-    // init host configs
-    for (Long hostId : hostsById.keySet()) {
-      try {
-        m_agentConfigsHolder.get().initializeDataIfNeeded(hostId, true);
-      } catch (AmbariRuntimeException e) {
-        LOG.error("Agent configs initialization was failed", e);
-      }
+    // init host configs in parallel on the shared default ForkJoinPool; the 
per-batch cache
+    // must be concurrent since it is populated from multiple threads.
+    Map<Long, Map<String, DesiredConfig>> cachedClustersDesiredConfigs = new 
ConcurrentHashMap<>();
+    try {
+      threadPools.getDefaultForkJoinPool().submit(() ->
+        hostsById.keySet().parallelStream().forEach(hostId -> {
+          try {
+            m_agentConfigsHolder.get().initializeDataIfNeeded(hostId, true, 
cachedClustersDesiredConfigs);
+          } catch (AmbariRuntimeException e) {
+            LOG.error("Agent configs initialization was failed", e);
+          }
+        })
+      ).get();
+    } catch (InterruptedException e) {
+      Thread.currentThread().interrupt();
+      LOG.error("Agent configs initialization was failed", e);
+    } catch (ExecutionException e) {
+      LOG.error("Agent configs initialization was failed", e);
     }
   }
 
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/state/host/HostImpl.java 
b/ambari-server/src/main/java/org/apache/ambari/server/state/host/HostImpl.java
index 8702001c1a..7fd1a25fc2 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/state/host/HostImpl.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/state/host/HostImpl.java
@@ -1125,7 +1125,7 @@ public class HostImpl implements Host {
       }
     ));
 
-    Map<Long, ConfigGroup> configGroups = (cluster == null) ? new HashMap<>() 
: cluster.getConfigGroupsByHostname(getHostName());
+    Map<Long, ConfigGroup> configGroups = (cluster == null) ? new HashMap<>() 
: cluster.getConfigGroupsByHostId(getHostId());
     if (configGroups == null || configGroups.isEmpty()) {
       return hostConfigMap;
     }
diff --git 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/STOMPComponentsDeleteHandler.java
 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/STOMPComponentsDeleteHandler.java
index 79dd94836e..7bd97cc501 100644
--- 
a/ambari-server/src/main/java/org/apache/ambari/server/topology/STOMPComponentsDeleteHandler.java
+++ 
b/ambari-server/src/main/java/org/apache/ambari/server/topology/STOMPComponentsDeleteHandler.java
@@ -18,7 +18,9 @@
 package org.apache.ambari.server.topology;
 
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 import java.util.TreeMap;
 import java.util.stream.Collectors;
@@ -35,6 +37,7 @@ import 
org.apache.ambari.server.controller.internal.DeleteHostComponentStatusMet
 import org.apache.ambari.server.events.TopologyUpdateEvent;
 import org.apache.ambari.server.events.UpdateEventType;
 import org.apache.ambari.server.state.Clusters;
+import org.apache.ambari.server.state.DesiredConfig;
 
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -95,13 +98,14 @@ public class STOMPComponentsDeleteHandler {
 
   private void updateNonTopologyAgentInfo(Set<Long> changedHosts, Long 
clusterId) throws AmbariException {
 
+    Map<Long, Map<String, DesiredConfig>> cachedClustersDesiredConfigs = new 
HashMap<>();
     for (Long hostId : changedHosts) {
       if (clusterId != null) {
         
alertDefinitionsHolder.get().updateData(alertDefinitionsHolder.get().getDeleteCluster(clusterId,
 hostId));
-        
agentConfigsHolder.get().updateData(agentConfigsHolder.get().getCurrentDataExcludeCluster(hostId,
 clusterId));
+        
agentConfigsHolder.get().updateData(agentConfigsHolder.get().getCurrentDataExcludeCluster(hostId,
 clusterId, cachedClustersDesiredConfigs));
         
hostLevelParamsHolder.get().updateData(hostLevelParamsHolder.get().getCurrentDataExcludeCluster(hostId,
 clusterId));
       } else {
-        
agentConfigsHolder.get().updateData(agentConfigsHolder.get().getCurrentData(hostId));
+        
agentConfigsHolder.get().updateData(agentConfigsHolder.get().getCurrentData(hostId,
 cachedClustersDesiredConfigs));
         
hostLevelParamsHolder.get().updateData(hostLevelParamsHolder.get().getCurrentData(hostId));
       }
     }
diff --git 
a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ConfigGroupResourceProviderTest.java
 
b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ConfigGroupResourceProviderTest.java
index 449d55047e..381eac154e 100644
--- 
a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ConfigGroupResourceProviderTest.java
+++ 
b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/ConfigGroupResourceProviderTest.java
@@ -582,6 +582,267 @@ public class ConfigGroupResourceProviderTest {
         configGroup, response, configGroupResponse, configHelper, hostDAO, 
hostEntity1, hostEntity2, h1, h2);
   }
 
+  @Test
+  public void testUpdateConfigGroupScopesToPreviousAndNewMemberHosts() throws 
Exception {
+    // The config group already has members h1 and h2; the update keeps only 
h1.
+    // Agent config update must be scoped to the union of previous and new 
members (h1, h2) so the
+    // removed host (h2) is reverted to the cluster-default config.
+    Authentication authentication = 
TestAuthenticationFactory.createAdministrator();
+
+    AmbariManagementController managementController = 
createMock(AmbariManagementController.class);
+    RequestStatusResponse response = 
createNiceMock(RequestStatusResponse.class);
+    ConfigHelper configHelper = createMock(ConfigHelper.class);
+    Clusters clusters = createNiceMock(Clusters.class);
+    Cluster cluster = createNiceMock(Cluster.class);
+    Host h1 = createNiceMock(Host.class);
+    Host h2 = createNiceMock(Host.class);
+    HostEntity hostEntity1 = createMock(HostEntity.class);
+
+    final ConfigGroup configGroup = createNiceMock(ConfigGroup.class);
+    ConfigGroupResponse configGroupResponse = 
createNiceMock(ConfigGroupResponse.class);
+
+    expect(cluster.isConfigTypeExists("core-site")).andReturn(true).anyTimes();
+    expect(managementController.getClusters()).andReturn(clusters).anyTimes();
+    expect(managementController.getAuthName()).andReturn("admin").anyTimes();
+    expect(clusters.getCluster("Cluster100")).andReturn(cluster).anyTimes();
+    expect(clusters.getHost("h1")).andReturn(h1);
+    expect(hostDAO.findByName("h1")).andReturn(hostEntity1).anyTimes();
+    expect(hostDAO.findById(1L)).andReturn(hostEntity1).anyTimes();
+    expect(hostEntity1.getHostId()).andReturn(1L).atLeastOnce();
+    expect(h1.getHostId()).andReturn(1L).anyTimes();
+    expect(h2.getHostId()).andReturn(2L).anyTimes();
+    
expect(managementController.getConfigGroupUpdateResults((ConfigGroupRequest) 
anyObject())).
+        andReturn(new ConfigGroupResponse(1L, "", "", "", "", new HashSet<>(), 
new HashSet<>())).atLeastOnce();
+
+    expect(configGroup.getName()).andReturn("test-1").anyTimes();
+    expect(configGroup.getId()).andReturn(25L).anyTimes();
+    expect(configGroup.getTag()).andReturn("tag-1").anyTimes();
+    // existing members of the group before the update
+    Map<Long, Host> existingHosts = new HashMap<>();
+    existingHosts.put(1L, h1);
+    existingHosts.put(2L, h2);
+    expect(configGroup.getHosts()).andReturn(existingHosts).anyTimes();
+    
expect(configGroup.convertToResponse()).andReturn(configGroupResponse).anyTimes();
+    
expect(configGroupResponse.getClusterName()).andReturn("Cluster100").anyTimes();
+    expect(configGroupResponse.getId()).andReturn(25L).anyTimes();
+
+    expect(cluster.getConfigGroups()).andStubAnswer(new IAnswer<Map<Long, 
ConfigGroup>>() {
+      @Override
+      public Map<Long, ConfigGroup> answer() throws Throwable {
+        Map<Long, ConfigGroup> configGroupMap = new HashMap<>();
+        configGroupMap.put(configGroup.getId(), configGroup);
+        return configGroupMap;
+      }
+    });
+
+    // capture the hosts the agent config update is scoped to
+    Capture<List<Long>> hostIdsCapture = newCapture();
+    configHelper.updateAgentConfigs(anyObject(Cluster.class), 
capture(hostIdsCapture));
+
+    replay(managementController, clusters, cluster, configGroup, response, 
configGroupResponse,
+        configHelper, hostDAO, hostEntity1, h1, h2);
+
+    ConfigGroupResourceProvider provider = (ConfigGroupResourceProvider)
+        
AbstractControllerResourceProvider.getResourceProvider(Resource.Type.ConfigGroup,
 managementController);
+    Provider<ConfigHelper> configHelperProvider = 
createNiceMock(Provider.class);
+    expect(configHelperProvider.get()).andReturn(configHelper).anyTimes();
+    replay(configHelperProvider);
+    Field m_configHelper = 
ConfigGroupResourceProvider.class.getDeclaredField("m_configHelper");
+    m_configHelper.setAccessible(true);
+    m_configHelper.set(provider, configHelperProvider);
+
+    Map<String, Object> properties = new LinkedHashMap<>();
+
+    Set<Map<String, Object>> hostSet = new HashSet<>();
+    Map<String, Object> host1 = new HashMap<>();
+    host1.put(ConfigGroupResourceProvider.HOST_NAME, "h1");
+    hostSet.add(host1);
+
+    Set<Map<String, Object>> configSet = new HashSet<>();
+    Map<String, String> configMap = new HashMap<>();
+    Map<String, Object> configs = new HashMap<>();
+    configs.put("type", "core-site");
+    configs.put("tag", "version100");
+    configMap.put("key1", "value1");
+    configs.put("properties", configMap);
+    configSet.add(configs);
+
+    properties.put(ConfigGroupResourceProvider.CLUSTER_NAME, "Cluster100");
+    properties.put(ConfigGroupResourceProvider.GROUP_NAME, "test-1");
+    properties.put(ConfigGroupResourceProvider.TAG, "tag-1");
+    properties.put(ConfigGroupResourceProvider.HOSTS, hostSet);
+    properties.put(ConfigGroupResourceProvider.DESIRED_CONFIGS, configSet);
+
+    Map<String, String> mapRequestProps = new HashMap<>();
+    mapRequestProps.put("context", "Called from a test");
+
+    Request request = PropertyHelper.getUpdateRequest(properties, 
mapRequestProps);
+
+    Predicate predicate = new PredicateBuilder().property
+        (ConfigGroupResourceProvider.CLUSTER_NAME).equals("Cluster100").and().
+        property(ConfigGroupResourceProvider.ID).equals(25L).toPredicate();
+
+    SecurityContextHolder.getContext().setAuthentication(authentication);
+
+    provider.updateResources(request, predicate);
+
+    verify(configHelper);
+    List<Long> capturedHostIds = hostIdsCapture.getValue();
+    assertEquals(2, capturedHostIds.size());
+    assertTrue(capturedHostIds.contains(1L));
+    assertTrue(capturedHostIds.contains(2L));
+  }
+
+  @Test
+  public void testCreateConfigGroupWithoutHostsSkipsAgentConfigUpdate() throws 
Exception {
+    // A config group created with no member hosts must not trigger an agent 
config update;
+    // an empty host list would otherwise fall back to updating every host in 
the cluster
+    // (see AgentConfigsHolder#updateData).
+    Authentication authentication = 
TestAuthenticationFactory.createAdministrator();
+
+    AmbariManagementController managementController = 
createMock(AmbariManagementController.class);
+    Clusters clusters = createNiceMock(Clusters.class);
+    Cluster cluster = createNiceMock(Cluster.class);
+    ConfigGroupFactory configGroupFactory = 
createNiceMock(ConfigGroupFactory.class);
+    ConfigGroup configGroup = createNiceMock(ConfigGroup.class);
+    ConfigHelper configHelper = createMock(ConfigHelper.class);
+
+    expect(managementController.getClusters()).andReturn(clusters).anyTimes();
+    expect(clusters.getCluster("Cluster100")).andReturn(cluster).anyTimes();
+    expect(cluster.getClusterName()).andReturn("Cluster100").anyTimes();
+    expect(cluster.isConfigTypeExists(anyString())).andReturn(true).anyTimes();
+    
expect(managementController.getConfigGroupFactory()).andReturn(configGroupFactory).anyTimes();
+    expect(managementController.getAuthName()).andReturn("admin").anyTimes();
+    expect(configGroupFactory.createNew(anyObject(Cluster.class), anyObject(), 
anyObject(),
+        anyObject(), anyObject(), anyObject(), 
anyObject())).andReturn(configGroup).anyTimes();
+
+    // configHelper.updateAgentConfigs is intentionally NOT expected; the 
strict mock fails if it is called.
+    replay(managementController, clusters, cluster, configGroupFactory, 
configGroup, configHelper);
+
+    ConfigGroupResourceProvider provider = (ConfigGroupResourceProvider)
+        
AbstractControllerResourceProvider.getResourceProvider(Resource.Type.ConfigGroup,
 managementController);
+    Provider<ConfigHelper> configHelperProvider = 
createNiceMock(Provider.class);
+    expect(configHelperProvider.get()).andReturn(configHelper).anyTimes();
+    replay(configHelperProvider);
+    Field m_configHelper = 
ConfigGroupResourceProvider.class.getDeclaredField("m_configHelper");
+    m_configHelper.setAccessible(true);
+    m_configHelper.set(provider, configHelperProvider);
+
+    Set<Map<String, Object>> propertySet = new LinkedHashSet<>();
+    Map<String, Object> properties = new LinkedHashMap<>();
+
+    Set<Map<String, Object>> configSet = new HashSet<>();
+    Map<String, String> configMap = new HashMap<>();
+    Map<String, Object> configs = new HashMap<>();
+    configs.put("type", "core-site");
+    configs.put("tag", "version100");
+    configMap.put("key1", "value1");
+    configs.put("properties", configMap);
+    configSet.add(configs);
+
+    properties.put(ConfigGroupResourceProvider.CLUSTER_NAME, "Cluster100");
+    properties.put(ConfigGroupResourceProvider.GROUP_NAME, "test-1");
+    properties.put(ConfigGroupResourceProvider.TAG, "tag-1");
+    // no HOSTS property: the group has no member hosts
+    properties.put(ConfigGroupResourceProvider.DESIRED_CONFIGS, configSet);
+    propertySet.add(properties);
+
+    Request request = PropertyHelper.getCreateRequest(propertySet, null);
+
+    SecurityContextHolder.getContext().setAuthentication(authentication);
+
+    provider.createResources(request);
+
+    // strict configHelper mock verifies updateAgentConfigs was never invoked
+    verify(configHelper);
+  }
+
+  @Test
+  public void testUpdateConfigGroupWithoutHostsSkipsAgentConfigUpdate() throws 
Exception {
+    // Updating a config group to have no member hosts (and it had none 
before) must not trigger an
+    // agent config update; an empty host list would otherwise fall back to 
updating every host in
+    // the cluster (see AgentConfigsHolder#updateData).
+    Authentication authentication = 
TestAuthenticationFactory.createAdministrator();
+
+    AmbariManagementController managementController = 
createMock(AmbariManagementController.class);
+    ConfigHelper configHelper = createMock(ConfigHelper.class);
+    Clusters clusters = createNiceMock(Clusters.class);
+    Cluster cluster = createNiceMock(Cluster.class);
+
+    final ConfigGroup configGroup = createNiceMock(ConfigGroup.class);
+    ConfigGroupResponse configGroupResponse = 
createNiceMock(ConfigGroupResponse.class);
+
+    expect(cluster.isConfigTypeExists("core-site")).andReturn(true).anyTimes();
+    expect(managementController.getClusters()).andReturn(clusters).anyTimes();
+    expect(managementController.getAuthName()).andReturn("admin").anyTimes();
+    expect(clusters.getCluster("Cluster100")).andReturn(cluster).anyTimes();
+    
expect(managementController.getConfigGroupUpdateResults((ConfigGroupRequest) 
anyObject())).
+        andReturn(new ConfigGroupResponse(1L, "", "", "", "", new HashSet<>(), 
new HashSet<>())).anyTimes();
+
+    expect(configGroup.getName()).andReturn("test-1").anyTimes();
+    expect(configGroup.getId()).andReturn(25L).anyTimes();
+    expect(configGroup.getTag()).andReturn("tag-1").anyTimes();
+    // the group has no members before the update, and the update sets none
+    expect(configGroup.getHosts()).andReturn(new HashMap<>()).anyTimes();
+    
expect(configGroup.convertToResponse()).andReturn(configGroupResponse).anyTimes();
+    
expect(configGroupResponse.getClusterName()).andReturn("Cluster100").anyTimes();
+    expect(configGroupResponse.getId()).andReturn(25L).anyTimes();
+
+    expect(cluster.getConfigGroups()).andStubAnswer(new IAnswer<Map<Long, 
ConfigGroup>>() {
+      @Override
+      public Map<Long, ConfigGroup> answer() throws Throwable {
+        Map<Long, ConfigGroup> configGroupMap = new HashMap<>();
+        configGroupMap.put(configGroup.getId(), configGroup);
+        return configGroupMap;
+      }
+    });
+
+    // configHelper.updateAgentConfigs is intentionally NOT expected; the 
strict mock fails if it is called.
+    replay(managementController, clusters, cluster, configGroup, 
configGroupResponse, configHelper);
+
+    ConfigGroupResourceProvider provider = (ConfigGroupResourceProvider)
+        
AbstractControllerResourceProvider.getResourceProvider(Resource.Type.ConfigGroup,
 managementController);
+    Provider<ConfigHelper> configHelperProvider = 
createNiceMock(Provider.class);
+    expect(configHelperProvider.get()).andReturn(configHelper).anyTimes();
+    replay(configHelperProvider);
+    Field m_configHelper = 
ConfigGroupResourceProvider.class.getDeclaredField("m_configHelper");
+    m_configHelper.setAccessible(true);
+    m_configHelper.set(provider, configHelperProvider);
+
+    Map<String, Object> properties = new LinkedHashMap<>();
+
+    Set<Map<String, Object>> configSet = new HashSet<>();
+    Map<String, String> configMap = new HashMap<>();
+    Map<String, Object> configs = new HashMap<>();
+    configs.put("type", "core-site");
+    configs.put("tag", "version100");
+    configMap.put("key1", "value1");
+    configs.put("properties", configMap);
+    configSet.add(configs);
+
+    properties.put(ConfigGroupResourceProvider.CLUSTER_NAME, "Cluster100");
+    properties.put(ConfigGroupResourceProvider.GROUP_NAME, "test-1");
+    properties.put(ConfigGroupResourceProvider.TAG, "tag-1");
+    // no HOSTS property: the update leaves the group with no member hosts
+    properties.put(ConfigGroupResourceProvider.DESIRED_CONFIGS, configSet);
+
+    Map<String, String> mapRequestProps = new HashMap<>();
+    mapRequestProps.put("context", "Called from a test");
+
+    Request request = PropertyHelper.getUpdateRequest(properties, 
mapRequestProps);
+
+    Predicate predicate = new PredicateBuilder().property
+        (ConfigGroupResourceProvider.CLUSTER_NAME).equals("Cluster100").and().
+        property(ConfigGroupResourceProvider.ID).equals(25L).toPredicate();
+
+    SecurityContextHolder.getContext().setAuthentication(authentication);
+
+    provider.updateResources(request, predicate);
+
+    // strict configHelper mock verifies updateAgentConfigs was never invoked
+    verify(configHelper);
+  }
+
   @Test
   public void testGetConfigGroupAsAmbariAdministrator() throws Exception {
     testGetConfigGroup(TestAuthenticationFactory.createAdministrator());
diff --git 
a/ambari-server/src/test/java/org/apache/ambari/server/state/ConfigHelperTest.java
 
b/ambari-server/src/test/java/org/apache/ambari/server/state/ConfigHelperTest.java
index 2955d1e1d7..136811daf6 100644
--- 
a/ambari-server/src/test/java/org/apache/ambari/server/state/ConfigHelperTest.java
+++ 
b/ambari-server/src/test/java/org/apache/ambari/server/state/ConfigHelperTest.java
@@ -363,6 +363,45 @@ public class ConfigHelperTest {
       Assert.assertEquals(expectedConfig_hiveServer1, 
originalConfig_hiveServer1);
     }
 
+    @Test
+    public void testGetHostsAffectedByConfigTypes() throws Exception {
+      Long h1 = clusters.getHost("h1").getHostId();
+      Long h2 = clusters.getHost("h2").getHostId();
+      Long h3 = clusters.getHost("h3").getHostId();
+      clusters.mapHostToCluster("h1", cluster.getClusterName());
+      clusters.mapHostToCluster("h2", cluster.getClusterName());
+      clusters.mapHostToCluster("h3", cluster.getClusterName());
+
+      // Empty changed types -> empty set (the caller falls back to all-hosts).
+      Assert.assertTrue(configHelper.getHostsAffectedByConfigTypes(cluster, 
new HashSet<String>()).isEmpty());
+
+      // A config type owned by no installed service (global, e.g. 
cluster-env) -> all hosts.
+      Set<String> globalType = new HashSet<>();
+      globalType.add("a-config-type-no-service-owns");
+      Set<Long> expectedAllHosts = new HashSet<>();
+      expectedAllHosts.add(h1);
+      expectedAllHosts.add(h2);
+      expectedAllHosts.add(h3);
+      Assert.assertEquals(expectedAllHosts, 
configHelper.getHostsAffectedByConfigTypes(cluster, globalType));
+
+      // cluster-env is a real stack-level config owned by no service -> must 
be treated as global.
+      Assert.assertNull("cluster-env must be unowned/global", 
cluster.getServiceByConfigType("cluster-env"));
+      Set<String> clusterEnv = new HashSet<>();
+      clusterEnv.add("cluster-env");
+      Assert.assertEquals(expectedAllHosts, 
configHelper.getHostsAffectedByConfigTypes(cluster, clusterEnv));
+
+      // hdfs-site is owned by the (installed) HDFS service, but no host runs 
an HDFS component yet -> empty.
+      Set<String> hdfsSite = new HashSet<>();
+      hdfsSite.add("hdfs-site");
+      Assert.assertTrue(configHelper.getHostsAffectedByConfigTypes(cluster, 
hdfsSite).isEmpty());
+
+      // Install an HDFS component on h1; hdfs-site now affects only h1.
+      
cluster.getService("HDFS").addServiceComponent("NAMENODE").addServiceComponentHost("h1");
+      Set<Long> expectedH1Only = new HashSet<>();
+      expectedH1Only.add(h1);
+      Assert.assertEquals(expectedH1Only, 
configHelper.getHostsAffectedByConfigTypes(cluster, hdfsSite));
+    }
+
     private Map<String, Map<String, String>> createHiveConfig() {
       return new HashMap<String, Map<String, String>>() {{
         put("hive-site", new HashMap<String, String>() {{
@@ -983,6 +1022,7 @@ public class ConfigHelperTest {
       // set up expectations
       expect(sch.getActualConfigs()).andReturn(schReturn).times(6);
       expect(sch.getHostName()).andReturn("h1").anyTimes();
+      expect(sch.getHost()).andReturn(clusters.getHost("h1")).anyTimes();
       expect(sch.getClusterId()).andReturn(cluster.getClusterId()).anyTimes();
       expect(sch.getServiceName()).andReturn("FLUME").anyTimes();
       
expect(sch.getServiceComponentName()).andReturn("FLUME_HANDLER").anyTimes();
@@ -1058,6 +1098,7 @@ public class ConfigHelperTest {
     // set up expectations
     expect(sch.getActualConfigs()).andReturn(schReturn).anyTimes();
     expect(sch.getHostName()).andReturn("h1").anyTimes();
+    expect(sch.getHost()).andReturn(clusters.getHost("h1")).anyTimes();
     expect(sch.getClusterId()).andReturn(cluster.getClusterId()).anyTimes();
     expect(sch.getServiceName()).andReturn("HDFS").anyTimes();
     expect(sch.getServiceComponentName()).andReturn("NAMENODE").anyTimes();


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

Reply via email to