[ 
https://issues.apache.org/jira/browse/KNOX-2900?focusedWorklogId=1041304&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-1041304
 ]

ASF GitHub Bot logged work on KNOX-2900:
----------------------------------------

                Author: ASF GitHub Bot
            Created on: 14/Sep/26 08:01
            Start Date: 14/Sep/26 08:01
    Worklog Time Spent: 10m 
      Work Description: smolnar82 commented on code in PR #1395:
URL: https://github.com/apache/knox/pull/1395#discussion_r4003172738


##########
gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzer.java:
##########
@@ -618,22 +751,33 @@ protected ServiceConfigurationModel 
getCurrentServiceConfiguration(final String
 
       ApiRoleConfigList roleConfigList = 
roleCollector.getAllServiceRoleConfigurations(clusterName, service);
 
-      for (ApiRoleConfig roleConfig : roleConfigList.getItems()) {
-        ApiConfigList configList = roleConfig.getConfig();
-
-        String roleName = roleConfig.getName();
-        String roleType = roleConfig.getRoleType();
-        ApiHostRef hostRef = roleConfig.getHostRef();
-        ApiRole role = new 
ApiRole().name(roleName).type(roleType).hostRef(hostRef);
-        roleConfigs.put(role, configList);
-      }
-      currentConfig = new ServiceConfigurationModel(svcConfig, roleConfigs);
+      final ApiService apiService = new 
ApiService().name(service).type(serviceType);

Review Comment:
   To reuse the discovery workflow, `getCurrentServiceConfiguration()` 
reconstructs the `ApiService` / `ApiRole` / config objects it feeds into 
`ServiceModelFactory.generateServiceModels()`. But this reconstruction doesn't 
populate the same fields that real discovery 
(`ClouderaManagerServiceDiscovery.discoverService`) hands to generators. Any 
`ServiceModelGenerator` that dereferences a field the reconstruction leaves 
null throws an NPE inside `generateService()`. This is not an edge case — it 
hits the majority of generators:
   
   - **`role.getHostRef().getHostname()`** — used by **34 of 51 generators** 
(`OozieServiceModelGenerator`, `SolrServiceModelGenerator`, HBase, Impala, and 
most others). NPEs if the reconstructed role's `hostRef` (or its hostname) 
isn't populated exactly as discovery populates it.
   - **`service.getClusterRef().getClusterName()`** — used by 
`YarnUIServiceModelGenerator` and `JobHistoryUIServiceModelGenerator`. NPEs 
because the synthetic `ApiService` sets only `name` / `type`, no `clusterRef`.
   - Any other field a given generator's `handles()` / `generateService()` 
reads that the reconstruction omits.
   
   **Failure path & blast radius:** a start/restart/scale event for almost any 
discoverable service (YARN, Oozie, Solr, HBase, …) → `hasConfigChanged` → 
`getCurrentServiceConfiguration` → `ServiceModelFactory.generateServiceModels` 
→ the generator's `generateService()` → **NPE**. The NPE is not an 
`ApiException`, so the method's own try/catch doesn't catch it; it propagates 
to `monitorClusterConfigurationChanges`' outer `catch(Exception)`, aborting the 
**entire** polling cycle for **all** clusters. Because the triggering event is 
never marked processed, the same NPE recurs every polling interval — PCA is 
effectively dead for that gateway.
   
   **Root cause / fix direction:** the reconstruction in 
`getCurrentServiceConfiguration()` diverges from how 
`ClouderaManagerServiceDiscovery` builds these objects. Rather than 
hand-rebuilding `ApiService` / `ApiRole` from `readServiceConfig`, PCA should 
obtain the model inputs through the **same** code path discovery uses (the 
shared component introduced here), so every field a generator may read is 
populated identically. A unit test that runs each registered generator against 
PCA-reconstructed inputs would have caught this and would guard against 
regressions as new generators land.



##########
gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzer.java:
##########
@@ -340,25 +344,31 @@ private boolean hasConfigChanged(String address, String 
clusterName, List<Releva
         // Get the previously-recorded configuration
         ServiceConfigurationModel serviceConfig = 
serviceConfigurations.get(re.getServiceType());
 
-        if (serviceConfig != null) {
-          // Get the current config for the started service, and compare with 
the previously-recorded config
-          ServiceConfigurationModel currentConfig =
-                          getCurrentServiceConfiguration(address, clusterName, 
re.getService());
-
-          if (currentConfig != null) {
-            log.analyzingCurrentServiceConfiguration(re.getService());
-            try {
-              configHasChanged = hasConfigurationChanged(serviceConfig, 
currentConfig);
-            } catch (Exception e) {
-              log.errorAnalyzingCurrentServiceConfiguration(re.getService(), 
e);
-            }
+        // Get the current (model-derived) config for the started service. 
This is null when the service produces no
+        // model (e.g. invalid configuration), just as such a service is 
absent from the recorded baseline.
+        ServiceConfigurationModel currentConfig =
+                        getCurrentServiceConfiguration(address, clusterName, 
re.getService(), re.getServiceType());
+
+        if (serviceConfig == null && currentConfig == null) {
+          // Was and remains in an invalid configuration state (no model 
either time): nothing to proxy, no change.
+          log.skippingConfigChangeForInvalidService(re.getService(), 
re.getServiceType());
+        } else if (serviceConfig != null && currentConfig != null) {
+          // Valid before and now: compare the recorded and current configs to 
detect a change.
+          log.analyzingCurrentServiceConfiguration(re.getService());
+          try {
+            configHasChanged = hasConfigurationChanged(serviceConfig, 
currentConfig);
+          } catch (Exception e) {
+            log.errorAnalyzingCurrentServiceConfiguration(re.getService(), e);
           }
-        } else {
-          // A new service (no prior config) represent a config change, since 
a descriptor may have referenced
-          // the "new" service, but discovery had previously not succeeded 
because the service had not been
-          // configured (appropriately) at that time.
+        } else if (currentConfig != null) {
+          // No prior config, but the service now produces a model: new / 
became valid -> re-discover.
           log.serviceEnabled(re.getService());
           configHasChanged = true;
+        } else {
+          // Had a prior config but produces no model now: became invalid / 
was removed -> re-discover so the
+          // service is dropped from the affected topologies (and the 
scoped-replace merge clears its baseline).
+          log.serviceDisabled(re.getService());
+          configHasChanged = true;

Review Comment:
   This new branch treats `getCurrentServiceConfiguration() == null` as "the 
service is now invalid / disabled" and forces re-discovery. But that method 
**also** returns `null` on any `ApiException` - network blip, auth failure, 
transient 5xx:
   
   ```java
   } catch (ApiException e) {
     log.clouderaManagerConfigurationAPIError(e);
   }
   return currentConfig; // still null on API error
   ```
   So a `null` return is ambiguous: it can mean either "config genuinely 
produced no model" or "couldn't reach CM". hasConfigChanged's else branch 
(prior config present, currentConfig == null) can't tell them apart and 
unconditionally sets configHasChanged = true, logs serviceDisabled, and 
triggers a full re-discovery.
   
   **Failure path:** CM API has a transient outage while computing the current 
config for a service that has a valid prior baseline → 
`getCurrentServiceConfiguration` catches the `ApiException` and returns `null` 
→ `hasConfigChanged` forces re-discovery → repeats every polling cycle until CM 
recovers.
   
   **Regression:** previously an API-error `null` was a harmless no-op 
(configHasChanged stayed false). This PR turns a transient CM error into a 
repeating full-cluster rediscovery storm — the same failure class KNOX-2900 set 
out to eliminate.
   
   **Fix direction:** distinguish "no model produced" from "CM unreachable". 
Options: let the `ApiException` propagate (or rethrow) instead of collapsing it 
to `null`, or return a tri-state / Optional so the caller can skip the change 
decision on API errors and only treat a genuinely empty-but-successful result 
as "service invalid".





Issue Time Tracking
-------------------

    Worklog Id:     (was: 1041304)
    Time Spent: 0.5h  (was: 20m)

> KNOX-2899 followup - reenable service based discovery filter
> ------------------------------------------------------------
>
>                 Key: KNOX-2900
>                 URL: https://issues.apache.org/jira/browse/KNOX-2900
>             Project: Apache Knox
>          Issue Type: Task
>            Reporter: Attila Magyar
>            Assignee: Tamás Marcinkovics
>            Priority: Major
>          Time Spent: 0.5h
>  Remaining Estimate: 0h
>
> In KNOX-2899 the service based discovery filter was temporary disabled 
> because it interferes with the polling configurator analyzer. 
> * Either a permanent fix is needed that works well with the polling config 
> analyizer
> * Or the filter should be permanently removed, if the performance gain would 
> be negligible
> cc: [~smolnar]



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to